Description
We have a setup where our spring boot app is connected to 2 separate datasources, primary and secondary. These datasources use different schemas. Also we try to reuse our spring properties as much as possible to avoid duplicating common properties. To this end we inject the primary jpa properties when creating the secondary jpa properties.
It seems that under some load orders this causes the primary jpa properties to be mutated. Or perhaps they are always mutated and it is simply not relevant if it happens after they've been used.
In any cause, when the primary jpa properties have been mutated by the time the primary entity manager factory is created then the primary entity manager will hold (at least some of) the second datasource's values, leading to errors like "secondary_schema.primary table does not exist".
This should probably not happen.
How to reproduce
- Download and import this example project as gradle project in your IDE
- Execute the tests in
DemoApplicationTest
Expected: Tests work (no assert)
Actual behavior:
primaryRepository.save(new PrimaryEntity()) throws an exception because secondary_schema is not found for the sql statement insert into secondary_schema.primary_entity ...
Further details
In the demo project the load order is: primary jpa properties > secondary jpa properties > secondary entity manager factory > primary entity manager factory
When adding a breakpoint to PrimaryDatabaseConfig.entityManagerFactory, SecondaryDatabaseConfig.entityManagerFactory and SecondaryDatabaseConfig.jpaProperties I can confirm, that
1. the defaultJpaProperties autowired into SecondaryDatabaseConfig.jpaProperties hold the correct default schema in their defaultJpaProperties.properties map
2. the secondaryJpaProperties autowired into SecondaryDatabaseConfig.entityManagerFactory are a different object that holds the correct values
3. the jpaProperties autowired into PrimaryDatabaseConfig.entityManagerFactory are the same object as the defaultJpaProperties autowired in step 1, only their properties fiel
d now hold a map with the default schema secondary_schema
So it seems to me that injecting the primary jpa properties into the secondary jpa property bean definition mutates the primary jpa properties. I believe this should not happen.
The behavior even persists when introducing an additional layer of indirection. Defining a class
@Component
public class JpaPropertiesContainer {
private Map<String, String> properties;
public JpaPropertiesContainer(JpaProperties jpaProperties) {
this.properties = jpaProperties.getProperties();
}
public Map<String, String> getProperties() {
return properties;
}
}
and autowiring this class into SecondaryDatabaseConfig.jpaProperties causes the same behavior, so the primary jpa properties get mutated even when they are not autowired directly into the secondary jpa properties as a parameter. Autowiring nothing into SecondaryDatabaseConfig.jpaProperties solves the issue, however. Why? I have no idea...
Edit: I just saw that I accidentally had 2 demo projects in the archive. Only the project demo-3-1-2 is relevant, the one called demo is not. I've reuploaded the file to contain the relevant project only, so any downloads from here on should be fine. Otherwise just ignore the project called demo
Comment From: rajadilipkolli
Hi @1dEraNCeSIv0 , Can you once check https://github.com/rajadilipkolli/my-spring-boot-experiments/tree/main/jpa/boot-data-multipledatasources , this is what you are looking, I guess.
Comment From: 1dEraNCeSIv0
Hi @rajadilipkolli, skimming your link it seems like an example project demonstrating one way to set up spring boot with 2 datasources. Judging by how it is set up I don't expect them to run into the same issue, as they don't share common jpa properties between their datasources.
I think there's 2 points at play here: 1. Should our 2 datasource setup be how it is (i.e. it could be more like in your link) 1. Should there be load order dependent jpa property mutation
While I think the first might be up for debate, the answer to the second should definitely be "no". Merely injecting a bean should not mutate it unless you mutate it yourself, would you not agree?
So it seems that, regardless of whether or not our 2 db setup could be different (and therefore not run into this issue), there is still something going on that's probably not right.
Comment From: wilkinsona
Thanks for the sample. While I think it would be worthwhile for us to figure out the cause of the apparent mutation of the properties, please note that using JpaProperties or any other @ConfigurationProperties class as you have done is not supported:
The properties that map to
@ConfigurationPropertiesclasses available in Spring Boot, which are configured through properties files, YAML files, environment variables, and other mechanisms, are public API but the accessors (getters/setters) of the class itself are not meant to be used directly.
Comment From: 1dEraNCeSIv0
I'm not sure I understand what exactly should not be done. My best guess is that spring boot classes that come with @ConfigurationProperty like JpaProperties can be used outside of spring boot but their accessors should not be used. The class only has accessors, so I guess it follows that the class should not be used at all? Is this a correct reading of the quoted paragraph?
The reason I've used it was that I was fairly confident that if properties get renamed then spring boot's JpaProperties class would probably be updated as well to reflect that, saving me from having to do it.
But I've tried a second version where I do not use JpaProperties like this. The error persists, see here for the adjusted version of the project.
Comment From: wilkinsona
Is this a correct reading of the quoted paragraph?
Yes, that's correct. The getters and setter on @ConfigurationProperties classes are really intended only for Boot's own use and are only public out of necessity.
Comment From: 1dEraNCeSIv0
Thanks for clarifying 🙂
Comment From: wilkinsona
The cause of the problem lies in SecondaryJpaProperties. Its properties map is the same HashMap instance as the properties map of PrimaryJpaProperties:
SecondaryJpaProperties(PrimaryJpaProperties primaryJpaProperties) {
this.properties = primaryJpaProperties.getProperties();
Both spring.jpa.properties.hibernate.default_schema=primary_schema and secondary.spring.jpa.properties.hibernate.default_schema=secondary_schema are being bound to this map so the value of hibernate.default_schema varies depending on when you query the map and what binding has been performed at that time. You can avoid the problem by copying the map:
SecondaryJpaProperties(PrimaryJpaProperties primaryJpaProperties) {
this.properties = new HashMap<>(primaryJpaProperties.getProperties());
With this change in place, DemoApplicationTests passes.
Comment From: 1dEraNCeSIv0
Ah, could've seen that. Thanks for making me aware and sorry for wasting your time.