I have created sample project where i configured two datalayer's providers, blocking H2 in-memory and reactive H2 in memory: (checkout commit "blocking vs reactive refs, 6847756") https://github.com/ndopj/spring-boot-2.3.0-r2dbc-bug
Running application fails because ConnectionFactoryInitializer
and ReactiveTransactionManager
beans inside ManualReactiveConfiguration.class
are loaded later than @PostConstruct
method of service ReactiveService.class
. As opposite ManualBlockingConfiguration.class
contains @EnableJpaRepositories
annotation with options entityManagerFactoryRef
and transactionManagerRef
which i guess load correct beans before any usage of jpa-repositories. I would expect same options for reactiveTransactionManager
and ConnectionFactoryInitializer
in case of @EnableR2dbcRepositories
annotation.
Note: uncommenting controller under ApplicationLoader.class
and commenting out @PostConstruct
method of ReactiveService.class
makes application runnable as expected since required beans are lazily loaded at "spring boot runtime" (also compare output logs between two cases). I have tried to reconfigure order of configuration but that doesn't seem to work. I have been following documentation:
https://docs.spring.io/spring-data/r2dbc/docs/1.0.x/reference/html/#r2dbc.datbaseclient.transactions
to create manual configuration of r2dbc data layer beans.
Comment From: ndopj
I made a research little bit about this and found out following:
connectionFactory
bean (in this case renamed to reactiveConnectionFactoryBean) is not really picked but is used by AbstracR2dncConfiguration.class
to setup r2dbcDatabaseClient
bean. Other beans from my manual configuration are picked later.
Looking into @EnableJpaRepositories
annotation i mentioned we can see following:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(JpaRepositoriesRegistrar.class)
public @interface EnableJpaRepositories {
. . .
/**
* Configures the name of the {@link EntityManagerFactory} bean definition to be used to create repositories
* discovered through this annotation. Defaults to {@code entityManagerFactory}.
*
* @return
*/
String entityManagerFactoryRef() default "entityManagerFactory";
String transactionManagerRef() ......
. . .
}
then looking into JpaRepositoryConfigExtension.class
from org.springframework.data.jpa.repository.config
we can see following
/**
* JPA specific configuration extension parsing custom attributes from the XML namespace and
* {@link EnableJpaRepositories} annotation. Also, it registers bean definitions for a
* {@link PersistenceAnnotationBeanPostProcessor} (to trigger injection into {@link PersistenceContext}/
* {@link PersistenceUnit} annotated properties and methods) as well as
* {@link PersistenceExceptionTranslationPostProcessor} to enable exception translation of persistence specific
* exceptions into Spring's {@link DataAccessException} hierarchy.
*
* @author Oliver Gierke
* @author Eberhard Wolff
* @author Gil Markham
* @author Thomas Darimont
* @author Christoph Strobl
* @author Mark Paluch
*/
public class JpaRepositoryConfigExtension extends RepositoryConfigurationExtensionSupport {
private static final Class<?> PAB_POST_PROCESSOR = PersistenceAnnotationBeanPostProcessor.class;
private static final String DEFAULT_TRANSACTION_MANAGER_BEAN_NAME = "transactionManager";
private static final String ENABLE_DEFAULT_TRANSACTIONS_ATTRIBUTE = "enableDefaultTransactions";
private static final String JPA_METAMODEL_CACHE_CLEANUP_CLASSNAME = "org.springframework.data.jpa.util.JpaMetamodelCacheCleanup";
private static final String ESCAPE_CHARACTER_PROPERTY = "escapeCharacter";
. . .
@Override
public void postProcess(BeanDefinitionBuilder builder, RepositoryConfigurationSource source) {
Optional<String> transactionManagerRef = source.getAttribute("transactionManagerRef");
builder.addPropertyValue("transactionManager", transactionManagerRef.orElse(DEFAULT_TRANSACTION_MANAGER_BEAN_NAME));
builder.addPropertyValue("entityManager", getEntityManagerBeanDefinitionFor(source, source.getSource()));
builder.addPropertyValue(ESCAPE_CHARACTER_PROPERTY, getEscapeCharacter(source).orElse('\\'));
builder.addPropertyReference("mappingContext", JPA_MAPPING_CONTEXT_BEAN_NAME);
}
. . .
}
so i guess this is how JpaRepositoryConfigExtension.class
ensures that correct beans are picked whenever JpaRepository is used. In my case no such configuration exists and until my manual beans are picked default one's are used.
I would make a pull request but don't know if i am allowed to and this go to spring-framework dependencies probably as well. My temporary workaround is just not using reactive repositories inside @PostConstruct
methods.
Comment From: snicoll
@ndopj thanks for the sample but please consider trying to trim it down next time. I'd also appreciate if you wouldn't use Java 14 at it is not required at all to reproduce the problem.
First of all, mixing R2DBC and JPA in the same app is highly unusual and will not give you the right semantics
Running application fails because
Rather than describing what you think is the cause, please describe the failure. I don't even know what the actual problem and stacktrace is. I've removed the blocking
package and the reference to Spring Data JPA and the project still fails for me so I don't really understand how that's relevant.
This is what I have
2020-05-29 14:34:26.731 WARN 95435 --- [ main] onfigReactiveWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'reactiveService': Invocation of init method failed; nested exception is reactor.core.Exceptions$ErrorCallbackNotImplemented: org.springframework.data.r2dbc.BadSqlGrammarException: executeMany; bad SQL grammar [INSERT INTO model (name) VALUES ($1)]; nested exception is io.r2dbc.spi.R2dbcBadGrammarException: [42102] [42S02] Table "MODEL" not found; SQL statement:
INSERT INTO model (name) VALUES ($1) [42102-200]
2020-05-29 14:34:26.740 INFO 95435 --- [ main] ConditionEvaluationReportLoggingListener :
Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2020-05-29 14:34:26.749 ERROR 95435 --- [ main] o.s.boot.SpringApplication : Application run failed
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'reactiveService': Invocation of init method failed; nested exception is reactor.core.Exceptions$ErrorCallbackNotImplemented: org.springframework.data.r2dbc.BadSqlGrammarException: executeMany; bad SQL grammar [INSERT INTO model (name) VALUES ($1)]; nested exception is io.r2dbc.spi.R2dbcBadGrammarException: [42102] [42S02] Table "MODEL" not found; SQL statement:
INSERT INTO model (name) VALUES ($1) [42102-200]
at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization(InitDestroyAnnotationBeanPostProcessor.java:160) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization(AbstractAutowireCapableBeanFactory.java:416) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1788) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:595) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:517) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:323) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
Comment From: ndopj
Sorry for my issue structure, this is one of my first issues so i will do it better next time.
if you wouldn't use Java 14 at it is not required at all to reproduce the problem: as stated, sample isn't using any java 14 features, so one can just switch java version inside build.gradle (writing this for future readers)
First of all, mixing R2DBC and JPA in the same app is highly unusual and will not give you the right semantics:
it might be seen as bad practice, but since r2dbc project supports only few DB vendors now and some JDBC drivers might not even have support in future i guess that lot of people will have to solve such problems with mixing blocking and reactive data-layers. Note documentation on dispatching blocking calls to another thread pool: https://projectreactor.io/docs/core/release/reference/#schedulers and the part Schedulers.boundedElastic() is a handy way to give a blocking process its own thread so that it does not tie up other resources
. For example, in our project we are using not common DB provider (AWS Athena), i have manged to get it working with blocking spring boot data JPA starter. It would be shame to build whole project blocking just because of one DB provider and not benefit from reactive stack at all since blocking calls can be dispatched or somehow simulated as asynchronous (i now that dispatching to thread pool workaround is not real async or reactive behavior).
I am sorry that i forgot to provide my stack trace:
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'reactiveService': Invocation of init method failed; nested exception is reactor.core.Exceptions$ErrorCallbackNotImplemented: org.springframework.data.r2dbc.BadSqlGrammarException: executeMany; bad SQL grammar [INSERT INTO model (name) VALUES ($1)]; nested exception is io.r2dbc.spi.R2dbcBadGrammarException: [42102] [42S02] Tabuľka "MODEL" nenájdená
Table "MODEL" not found; SQL statement:
INSERT INTO model (name) VALUES ($1) [42102-200]
at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization(InitDestroyAnnotationBeanPostProcessor.java:160) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization(AbstractAutowireCapableBeanFactory.java:416) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1788) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:595) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:517) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:323) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:226) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:321) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
as you can see its exactly same as yours so configuration of blocking data layer doesn't affect behavior in this case at all. Note: i have updated git repository with transactionManager
references so blocking and reactive transaction manager beans doesn't clash when called since they both share same interface.
Exception happens because ConnectionFactoryInitializer
bean from ManualReactiveConfiguration.class
responsible for creating tables is not picked at the time of call to @PostConstruct init()
method from service ReactiveService.class
. As opposite i introduced how @EnableJpaRepositories
annotation provides support for referencing which EntityManagerFactory
and TransactionManager
beans should be picked as default ones so when JPA call is executed, correct beans are loaded. This references also provide support for multiple blocking data layer configurations (from @EnableJpaRepositories
referenced EntityManagerFactory
bean is used in Repository
interfaces placed in packages for which that bean configuration was created). Note the basePackages
(where this configuration should be used) and entityManagerFactoryRef
(which EntityManagerFactory should be used in that packages):
. . .
@EnableJpaRepositories(basePackages = {"com.blocking"},
entityManagerFactoryRef = "blockingEntityManagerFactory",
transactionManagerRef = "blockingTransactionManager")
public class ManualBlockingConfiguration {
. . .
}
Update: In mean time i have found workaround directly from documentation that references this problem: https://docs.spring.io/spring-data/r2dbc/docs/1.0.x/reference/html/#r2dbc.init and found also my own workaround which in my case is more easy to do then proposed ones by doing following:
@Configuration
@EnableTransactionManagement
@PropertySource(value = "classpath:application.properties")
@EnableR2dbcRepositories(basePackages = {"com.reactive"})
public class ManualReactiveConfiguration extends AbstractR2dbcConfiguration {
. . .
@Primary
@Bean("reactiveConnectionFactoryInitializer")
public ConnectionFactoryInitializer initializer(ConnectionFactory reactiveConnectionFactory) {
. . .
}
. . .
}
and inside class using reactive repository interface within @PostConstruct
mehtod (note @DependsOn
annotation):
@Service
@DependsOn("reactiveConnectionFactoryInitializer", "reactiveTransactionManager")
@Transactional(transactionManager = "reactiveTransactionManager")
public class ReactiveService {
. . .
@PostConstruct
void init() {
reactiveRepository.save(new ReactiveModel())
.subscribe(reactiveModel -> LoggerFactory.getLogger(this.getClass())
.info("Saved reactive model with ID {}", reactiveModel.getId()));
}
}
This structure will force to load ConnectionFactoryInitializer
bean before @PostConstruct init()
method is loaded. On the other hand it has disadvantage of specifying @DependsOn
annotation at each similar case. So you may close this issue after-all, but i don't see any reason why @EnableR2dbcRepositories
should not contain ConnectionFactoryInitalizerRef
option which would "bind" provided ConnectionFactoryInitializer
bean with that specific R2dbc
configuration over specific package and specific ConnectionFactory
so its instantiated at right time and not on its own, similarly to how transactionManagerRef
and entityManagerRef
works in case of @EnableJpaRepositories
. That would ease development and no mentioned workaround would be needed.
Note that this is my final stack trace (see "[blocking data config]" and "[reactive data config]" logs. While blocking configurations does correct bean instantiation thanks to mentioned Refs options, in case of reactive configuration i need to use @DependsOn
annotation to force pre-loading correct beans):
2020-05-30 19:01:18.405 INFO 21360 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 40ms. Found 1 JPA repository interfaces.
2020-05-30 19:01:18.406 INFO 21360 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Multiple Spring Data modules found, entering strict repository configuration mode!
2020-05-30 19:01:18.406 INFO 21360 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data R2DBC repositories in DEFAULT mode.
2020-05-30 19:01:18.412 INFO 21360 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 5ms. Found 1 R2DBC repository interfaces.
2020-05-30 19:01:18.788 INFO 21360 --- [ main] uration$$EnhancerBySpringCGLIB$$1427ada8 : [blocking data config] blocking datasource init
2020-05-30 19:01:18.847 INFO 21360 --- [ main] uration$$EnhancerBySpringCGLIB$$1427ada8 : [blocking data config] blocking entity manager factory init
2020-05-30 19:01:18.868 INFO 21360 --- [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: blocking]
2020-05-30 19:01:18.898 INFO 21360 --- [ main] org.hibernate.Version : HHH000412: Hibernate ORM core version 5.4.15.Final
2020-05-30 19:01:18.989 INFO 21360 --- [ main] o.hibernate.annotations.common.Version : HCANN000001: Hibernate Commons Annotations {5.1.0.Final}
2020-05-30 19:01:19.077 INFO 21360 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2020-05-30 19:01:19.149 INFO 21360 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2020-05-30 19:01:19.160 INFO 21360 --- [ main] org.hibernate.dialect.Dialect : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
2020-05-30 19:01:19.528 INFO 21360 --- [ main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
2020-05-30 19:01:19.532 INFO 21360 --- [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'blocking'
2020-05-30 19:01:19.693 INFO 21360 --- [ main] uration$$EnhancerBySpringCGLIB$$1427ada8 : [blocking data config] blocking transaction manager init
2020-05-30 19:01:19.721 INFO 21360 --- [ main] com.blocking.BlockingService : Save blocking model with ID 1
2020-05-30 19:01:19.726 INFO 21360 --- [ main] uration$$EnhancerBySpringCGLIB$$d4b64e28 : [reactive data config] Reactive connection factory init
2020-05-30 19:01:19.733 INFO 21360 --- [ main] uration$$EnhancerBySpringCGLIB$$d4b64e28 : [reactive data config] Reactive connection factory initializer init
2020-05-30 19:01:19.799 INFO 21360 --- [ main] uration$$EnhancerBySpringCGLIB$$d4b64e28 : [reactive data config] reactive transaction manager init
2020-05-30 19:01:19.914 INFO 21360 --- [ main] com.reactive.ReactiveService : Saved reactive model with ID 1
2020-05-30 19:01:20.227 INFO 21360 --- [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port(s): 8080
2020-05-30 19:01:20.235 INFO 21360 --- [ main] com.ApplicationLauncher : Started ApplicationLauncher in 3.178 seconds (JVM running for 3.734)
Comment From: snicoll
as stated, sample isn't using any java 14 features
Then don't require that source code level please, it just makes it harder for someone to run the sample if they don't have installed Java 14 yet.
In mean time i have found workaround directly from documentation that references this problem
It isn't a workaround but how you are supposed to configure things. If you want a certain bean to be initialized before another one you need to declare a dependency one way or the other. The Spring Boot reference guide has also a dedicated section that showcases how to initialize the database.