Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatically detect or warn when JPA repositories lack transactionManagerRef #34185

Open
devMtn30 opened this issue Jan 2, 2025 · 0 comments
Labels
status: waiting-for-triage An issue we've not yet triaged or decided on

Comments

@devMtn30
Copy link

devMtn30 commented Jan 2, 2025

Describe the issue

Hello Spring Team,

My name is kimsan. I've recently encountered a common configuration pitfall when using multiple transaction managers (for example, mainDBTransactionManager for MyBatis/JDBC and jpaTransactionManager for JPA).

Even though I annotated my service with @Transactional(transactionManager = "jpaTransactionManager"), there are scenarios where the internal qualifier becomes an empty string (""), causing Spring to look for a bean named "transactionManager". This leads to an exception like:

NoSuchBeanDefinitionException: No bean named 'transactionManager' available

After investigation, I found that the root cause was that I forgot to specify transactionManagerRef = "jpaTransactionManager" in @EnableJpaRepositories, combined with having a @Primary transaction manager for MyBatis.

This configuration issue often goes unnoticed until a write operation triggers a real transaction boundary.


Sample Project / Reproducer

Below is a minimal sample illustrating how this can happen:

  1. Two Transaction Managers

    • mainDBTransactionManager (marked as @Primary)
    • jpaTransactionManager (no @Primary)
  2. Repositories

    • A JPA CouponRepository extending JpaRepository (in a kr.co.example.jpa package)
  3. Configuration

    • @EnableJpaRepositories(basePackages = "kr.co.example.jpa")
      • Without transactionManagerRef = "jpaTransactionManager"
Gradle Project Structure (Click to expand)
└── src
    ├── main
    │   ├── java
    │   │   └── kr/co/example
    │   │       ├── MyBatisConfig.java
    │   │       ├── JpaDBConfig.java
    │   │       ├── CouponRepository.java
    │   │       └── CouponService.java
    │   └── resources
    └── test
        └── java
// MyBatisConfig.java
@Configuration
public class MyBatisConfig {

    @Bean
    @Primary
    public PlatformTransactionManager mainDBTransactionManager(@Qualifier("mainDataSource") DataSource ds) {
        return new DataSourceTransactionManager(ds);
    }

}
// JpaDBConfig.java
@Configuration
@EnableJpaRepositories(
   basePackages = "kr.co.example.jpa",
   // transactionManagerRef = "jpaTransactionManager" // <-- This is missing!!
)
public class JpaDBConfig {

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(/* ... */) {
        // ...
    }

    @Bean(name = "jpaTransactionManager")
    public JpaTransactionManager jpaTransactionManager(EntityManagerFactory emf) {
        return new JpaTransactionManager(emf);
    }
}
// CouponRepository.java
@Repository
public interface CouponRepository extends JpaRepository<Coupon, Long> {
    // ...
}
// CouponService.java
@Service
@Transactional(transactionManager = "jpaTransactionManager")
public class CouponService {

    @Autowired
    private CouponRepository couponRepository;

    public void saveCoupon(Coupon coupon) {
        // On 'save', occasionally fails because it tries to use the "" manager
        couponRepository.save(coupon);
    }

    public Coupon findCoupon(Long id) {
        // Read operations often *appear* to work or skip transaction
        return couponRepository.findById(id).orElse(null);
    }
}

Steps to Reproduce

Clone or create a project with two different transaction managers: one @primary, one for JPA.

Omit transactionManagerRef = "jpaTransactionManager" in @EnableJpaRepositories.

Run saveCoupon() in CouponService which is annotated with @transactional(transactionManager="jpaTransactionManager").

Observe that under certain conditions (especially if there's a prior transaction opened by the primary manager or if the service boundary triggers a new transaction incorrectly), Spring attempts to look up "transactionManager" instead of "jpaTransactionManager".

Get an exception like:

org.springframework.beans.factory.NoSuchBeanDefinitionException:
No bean named 'transactionManager' available
Expected behavior

Ideally, if a JPA repository is in use and we specify @transactional(transactionManager="jpaTransactionManager"), I'd expect Spring to either:
Always respect that if jpaTransactionManager bean is available, or
Provide a clear error or warning if transactionManagerRef is missing in @EnableJpaRepositories.
Possible Solutions or Feature Requests

Option A: Automatic detection/fallback

If TransactionAspectSupport detects that the target class is a JPA-based repository (e.g., implements JpaRepositoryImplementation) and no explicit manager is matched, fallback to "jpaTransactionManager" if present.
Pro: Helps new users avoid a common misconfiguration.
Con: Potentially confusing in multi-JPA setups.
Option B: Warning message or exception at startup

If @EnableJpaRepositories finds multiple PlatformTransactionManager beans but no transactionManagerRef, log a WARN-level message indicating possible misconfiguration.
Pro: Does not override user config; helps them fix the setup.
Con: Doesn’t “auto-fix.”
Option C: Clearer documentation

Emphasize in the reference docs that “When multiple transaction managers exist, you must specify transactionManagerRef.”
Pro: Straightforward approach.
Con: People often skip docs.
Additional Test Case

Here’s a simplified JUnit test that demonstrates how a save() call may fail if the manager is misapplied:

@SpringBootTest
class CouponServiceTest {

    @Autowired
    CouponService couponService;

    @Test
    void testSaveCoupon_ThrowsNoSuchBeanDefinitionException() {
        Coupon coupon = new Coupon("TestCoupon");
        assertThrows(NoSuchBeanDefinitionException.class, () -> {
            couponService.saveCoupon(coupon);
        });
    }
}

In a properly configured environment (with transactionManagerRef = "jpaTransactionManager"), this test should pass and save the entity.
In the faulty config, it fails because Spring tries to find "transactionManager".
Why This Matters

Many users combine MyBatis + JPA and run into this exact scenario.
The error messages can be cryptic for newcomers, leading to extra debugging time.
A built-in fallback or clear warning could greatly improve developer experience.

Is the Spring Team open to adding an auto-detection fallback? Or would you prefer a warning approach?
Is there any historical context for why transactionManagerRef must be explicitly set in multi-manager scenarios without an automatic fallback?
I’d be happy to contribute a PR with any of these approaches if the community finds them valuable.

Thank you for your time!

Best regards,
kimsan

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Jan 2, 2025
@snicoll snicoll changed the title [Enhancement Proposal] Automatically detect or warn when JPA repositories lack transactionManagerRef Automatically detect or warn when JPA repositories lack transactionManagerRef Jan 2, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: waiting-for-triage An issue we've not yet triaged or decided on
Projects
None yet
Development

No branches or pull requests

2 participants