aileftech / snap-admin

A plug-and-play, auto-generated CRUD database admin panel for Spring Boot apps
MIT License
251 stars 18 forks source link

Issue when using @Transactional #16

Closed fabienfleureau closed 10 months ago

fabienfleureau commented 11 months ago

Describe the bug When we use the @Transactional annotation, it fails at runtime saying there are two TransactionManager available (NoUniqueBeanDefinitionException)

Is the bug at startup before you perform any action? No, it's at runtime when going to a @Transactional annotated method

Stacktrace No qualifying bean of type 'org.springframework.transaction.TransactionManager' available: expected single matching bean but found 2: transactionManager,internalTransactionManager


org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'org.springframework.transaction.TransactionManager' available: expected single matching bean but found 2: transactionManager,internalTransactionManager
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1299)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveBean(DefaultListableBeanFactory.java:484)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:339)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:332)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.determineTransactionManager(TransactionAspectSupport.java:506)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:345)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:751)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:703)
    at myMethodWithTransactionalAnnotation```
aileftech commented 11 months ago

Hi fabien,

This happens because we register an internal transaction manager for the secondary data source. I think the issue should go away if you specify which transaction manager to use, like:

@Transactional("transactionManager") // This is the "default" one, registered by your application

I'd ask you to try this and see if it goes away. I'm not sure how to apply this setting globally/by default in order not to have to change all @Transactional, but if this solution works I can investigate.

EDIT: Another solution could be to use the @Primary annotation on the default transaction manager, but since usually the transaction manager is not defined in the code but instantiated automatically by Spring I'm not sure how to apply that yet (but if you defined the transactionManager bean this should be applicable).

fabienfleureau commented 11 months ago

Hello, indeed it works like this, but I would be nice that integrating dbadmin would not require any modification in existing bean configuration. (I have a lot of existing @Transactional, I won't modify all of them) As a workaround I did anotate with @Primary annotation

    @Bean
    @Primary
    @ConditionalOnMissingBean(TransactionManager::class)
    fun transactionManager(
        transactionManagerCustomizers: ObjectProvider<TransactionManagerCustomizers>
    ): PlatformTransactionManager {
        return JpaTransactionManager()
            .apply { transactionManagerCustomizers.ifAvailable {
                it.customize(this)
            }
        }
    }
aileftech commented 11 months ago

Yes, indeed it's ideal to not require any modification. But I think at least adding @Primary is required. Are you aware of any other possibility I can look into to avoid having to add that too?

aileftech commented 11 months ago

I've read a little bit more about this. Ideally I would need something like a @Secondary/@NotPrimary annotation, in order to make the other bean @Primary "by default" without having the user specify it.

From my limited knowledge such an annotation doesn't exist. These issues might be related so I'm saving them for future reference:

https://github.com/spring-projects/spring-framework/issues/26528#issuecomment-1149741516

https://github.com/spring-projects/spring-framework/issues/26241

fabienfleureau commented 10 months ago

There are way to instantiate the JPARepository programmatically without the needs of beans. Be doing there will be no conflicts with the main application beans. Also one possibility to could be not to use JPA at all for the internal processes and use jdbc related (you only have less than 5 different requests to write)

aileftech commented 10 months ago

The previous version was using JdbcTemplate indeed, but this brings many problems because table/field names can vary a lot with annotations, naming strategies, etc... and it's hard to get a solid result since you have to specify the field names "manually" if writing queries.

There might be a way to get the correct names by querying some Spring class, but I then decided to rewrite using JPA and solve all of this at once.

I will look into instantiating them without making them beans.

aileftech commented 10 months ago

This should be fixed with this latest commit on the dev branch. If you can confirm it as well on your side it would be great, @fabienfleureau ! (You should be able to remove the @Primary annotation and it should keep working)