I'm having a reactive Spring Boot 2.7.4 app using jOOQ + R2DBC, but also Flowable (which only works with JDBC).
Since a DataSource is not auto-configured in the presence of ConnectionFactory I'm forcing it in the following way (this is the most elegant approach I worked out, I'm open to suggestions):
@Configuration
// import unconditionally, otherwise not auto-configured in the presence of R2DBC connection factory
@Import(DataSourceAutoConfiguration.class)
public class DataSourceConfig {
}
This all worked well until I tried upgrading to Spring Boot 3.1.0. The issue is that DataSourceTransactionManagerAutoConfiguration.JdbcTransactionManagerConfiguration#transactionManager no longer matches.
After a few days of investigation, I wouldn't say it's a regression, but rather it used to work by accident. There is no explicit order between DataSourceTransactionManagerAutoConfiguration and R2dbcTransactionManagerAutoConfiguration and the former is affected by the latter.
IMO this should be fixed by replacing @ConditionalOnMissingBean(TransactionManager.class) (on top of DataSourceTransactionManagerAutoConfiguration.JdbcTransactionManagerConfiguration#transactionManager) with @ConditionalOnMissingBean(PlatformTransactionManager.class). I see no point to keep disabling JDBC auto-configurations (in the presence of the R2DBC equivalent beans) once DataSource is present.
Furthermore, I wasn't able to find anywhere an example of how to work with both JDBC and R2DBC in the same application context. Perhaps you should consider adding one.
Comment From: wilkinsona
Duplicates https://github.com/spring-projects/spring-boot/pull/35261.
Comment From: GeorgiPetkov
@wilkinsona It's partially duplicate.
- If you won't be making the proposed change then the nondeterministic order of the 2 auto configurations remains. The point is that even if you insist on having just 1 transaction manager it's not necessarily happening unless I'm missing something implicit here. There might be other such cases as well.
- An example/advice on how to mix JDBC and R2DBC is still very valuable. Everyone has to figure it out for himself and do hacks all over the place which will probably stop working after an upgrade. It's easy to say "this is not the standard way", but from a practical point of view it is very common - you might be slowly migrating big applications to the reactive world or you might be using integrations/libraries that simply don't work with R2DBC. From a user perspective, you're pretty much saying that one should base/split his microservices and architecture so it fits Spring's defaults (or go with the hacks). All I'm saying is that just trying to give such an example might be helpful to see the exact pain point from the user's perspective and perhaps this can even change your views. At least having a standard way to solve this problem will reduce the chance of regressions because otherwise, everyone will be solving it in a different way.
Comment From: wilkinsona
If you won't be making the proposed change then the nondeterministic order of the 2 auto configurations remains
The ordering shouldn't be non-deterministic. When there is nothing else that affects the ordering of two auto-configurations, they will be ordered alphabetically. In this case that should mean that, when the context contains both a DataSource and an R2DBC ConnectionFactory, both a PlatformTranactionManager and a ReactiveTransactionManager will be auto-configured.
I have just verified this with 3.1.0 and things behave as expected. I used this application:
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Import;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.ReactiveTransactionManager;
@SpringBootApplication
@Import(DataSourceAutoConfiguration.class)
public class Gh35937Application {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Gh35937Application.class, args);
System.out.println(context.getBean(PlatformTransactionManager.class));
System.out.println(context.getBean(ReactiveTransactionManager.class));
}
}
And these dependencies:
implementation 'org.springframework.boot:spring-boot-starter-data-r2dbc'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'io.r2dbc:r2dbc-h2'
On startup, it outputs the following:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.1.0)
2023-06-21T09:43:23.382+01:00 INFO 19135 --- [ main] com.example.demo.Gh35937Application : Starting Gh35937Application using Java 17.0.4.1 with PID 19135 (/Users/awilkinson/dev/workspaces/spring-projects/spring-boot/main/gh-35937/bin/main started by awilkinson in /Users/awilkinson/dev/workspaces/spring-projects/spring-boot/main/gh-35937)
2023-06-21T09:43:23.385+01:00 INFO 19135 --- [ main] com.example.demo.Gh35937Application : No active profile set, falling back to 1 default profile: "default"
2023-06-21T09:43:23.690+01:00 INFO 19135 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data R2DBC repositories in DEFAULT mode.
2023-06-21T09:43:23.700+01:00 INFO 19135 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 8 ms. Found 0 R2DBC repository interfaces.
2023-06-21T09:43:24.155+01:00 INFO 19135 --- [ main] com.example.demo.Gh35937Application : Started Gh35937Application in 1.017 seconds (process running for 1.404)
org.springframework.jdbc.support.JdbcTransactionManager@73545b80
org.springframework.r2dbc.connection.R2dbcTransactionManager@6d469831
Comment From: GeorgiPetkov
I meant that there is no explicit order and there should be one. "Non-deterministic" is not the right term, but it may change between versions and/or with different contexts. In my case, I've defined my own ConnectionFactory bean and probably this is what affects it (in fact I have 2 such beans). This is precisely how it worked for me as well on 2.7.4 and then changed in 3.1.0. Also, isn't this against the reasoning explained in the duplicate issue? In https://github.com/spring-projects/spring-boot/pull/35261 your team states that the intention is not to run the JDBC auto-configuration, but it does in the test given by you.
Comment From: wilkinsona
In https://github.com/spring-projects/spring-boot/pull/35261 your team states that the intention is not to run the JDBC auto-configuration, but it does in the test given by you.
@snicoll was talking about Boot's default behavior where the R2DBC auto-configuration kicking in switches off the DataSource auto-configuration. This aligns with our opinion that, generally speaking, JDBC and R2DBC should not be mixed in the same application. When you import @DataSourceAutoConfiguration (or define your own DataSource) in an app using R2DBC you're overriding this opinion. The ability to do that is a key feature of Spring Boot's auto-configuration and the convention over configuration approach.
I can't really comment on why the JdbcTransactionManager isn't being auto-configured when you have a DataSource bean as I don't know enough about what you're trying to do and how you've configured things.
If you would like us to spend any more time on this, please spend some time providing a complete yet minimal sample that reproduces the problem. You can share it with us by pushing it to a separate repository on GitHub or by zipping it up and attaching it to this issue.
Comment From: GeorgiPetkov
I see 2 options:
- you want to disable JDBC auto-configuration in case of Connectionfactory
- you want to allow more usages of the auto-configuration even when mixing JDBC and R2DBC
Currently, the first is not enforced (and you can do it with explicit order of the auto-configurations). This is also seen in your example app. The second one is the reported problem and we already mentioned the possible solution. See below why your example is also falling into the trap of "working by accident". So it's really neither one nor the other.
Regarding the example, using @Import(DataSourceAutoConfiguration.class) is a big hack because it pretty much imports the auto-configuration as a regular configuration and skips its conditions (and is not really often applicable as an approach because the condition might be on nested package-private class or method). This way it also has the side-effect of being loaded earlier and you "got lucky" that the transaction manager configurations are attempted to be auto-configured in the right order for it to work.
To show you how the auto-configurations' order is not really predictable, it will still work if you use @Import({DataSourceAutoConfiguration.class, R2dbcAutoConfiguration.class}), but it won't work with @Import({R2dbcAutoConfiguration.class, DataSourceAutoConfiguration.class}).
Comment From: wilkinsona
and skips its conditions
That's not the case. All of the conditions are still evaluated. The key difference is that importing the auto-configuration causes it to be treated as user configuration which is processed before any auto-configuration. As a result, when @ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory") is evaluated, there's no ConnectionFactory bean as it has not yet been auto-configured.
To show you how the auto-configurations' order is not really predictable, it will still work if you use
@Import({DataSourceAutoConfiguration.class, R2dbcAutoConfiguration.class}), but it won't work with@Import({R2dbcAutoConfiguration.class, DataSourceAutoConfiguration.class})
This shows a misunderstanding of how importing configuration classes works. The ordering here is 100% predictable – it is the order of the imports. As you are importing the two classes directly, they're no longer treated as auto-configuration classes and are no longer subject to the auto-configuration-specific ordering that's expressed by @AutoConfigureBefore, @AutoConfigureAfter, and @AutoConfigureOrder.
See below why your example is also falling into the trap of "working by accident".
Please don't take my "sample app" as a recommendation. It was an attempt to piece together an example of what you have described and show that it worked as I expected it to. A sample application of your own that shows exactly what you're trying to do and why it isn't working would have been really useful here.
you want to allow more usages of the auto-configuration even when mixing JDBC and R2DBC
As @snicoll and I have said, we don't want to do this. If you want to use R2DBC and JDBC in the same application, you can use auto-configuration for the R2DBC side of things. You'll then have to define beans for the JDBC side of things yourself. I would do this by defining a @Bean method that returns a DataSource rather than importing @DataSourceAutoConfiguration.
Comment From: GeorgiPetkov
Regarding the skipping on conditions - I can't say that I know better than you, I just assumed how it works. If they are not skipped it seems that I can define a configuration that wants a bean to not be present and then there's an auto-configuration that creates such a bean afterwards. Is this how it's supposed to work?
Back to the topic, I think you're the one misunderstanding here. The imports are for DataSource and ConnectionFactory auto-configurations and we're arguing that the order of the transaction manager auto-configurations is not predictable.
You have 2 auto-configurations (for the transaction managers), one clearly depends on the other, but this is not explicitly enforced. And the problem can be solved in 2 ways:
- make the order explicit (this seems to be aligned with your vision, for me there are no strong arguments, and AFAIK nothing is really depending on having a single TransactionManager, so it's rather a superficial constraint that you're trying to keep)
- don't have dependency between them, by not using the interface TransactionManager, but the interface PlatformTransactionManager (this is what the community wants and would benefit from)
I fair point for the second approach is that auto-configurations are much more easily turned off rather than forcefully turned on or partially reused.
Regarding the recommendation - it will be a brutal copy/paste of the auto-configurations' code and it won't be maintained while I already have your carefully written and maintained code available. It's not really a recommendation, in most cases it's the only way, for example, if needing DataSourceTransactionManagerAutoConfiguration.
Comment From: GeorgiPetkov
@wilkinsona Do you have any response on the above? Shouldn't we reopen this issue? Should I create new issues out of this?
Comment From: snicoll
Reading the back and forth on the issue, I believe Andy did his best to explain our points, which doesn't seem to satisfy you but I am afraid we'll have to leave it at that.
Comment From: GeorgiPetkov
As I said in my previous comment, Andy misunderstood the problem. He was mistaken about which auto-configurations are dependent on each other but may execute in any order.
You have a sample app reproducing the unpredictable/undesired behavior. It's what Andy provided himself with the simple change of the @Import annotation. Please read the thread (at least the second half), because I'll have to repeat myself again.