Reputation: 91
I have a question regarding Spring Boot and using a setup with multiple DataSources using JpaRepositories.
[4 Edits below]
The structure of the project looks like this:
com/mycompany/schema/AbstractJpaConfig
com/mycompany/schema/domain_A/AJpaConfiguration
com/mycompany/schema/domain_A/entity/AEntity
com/mycompany/schema/domain_A/repository/ARepository
com/mycompany/schema/domain_B/BJpaConfiguration
com/mycompany/schema/domain_B/entity/BEntity
com/mycompany/schema/domain_B/repository/BRepository
So I have two domains (A and B), with the DataSource setup being handled separately.
The abstract JPA configuration class is used to reduce redundancy and uses a custom DataSourceManager which handles the DataSources:
public abstract class AbstractJpaConfiguration {
private final DataSourceManager dataSources;
public AbstractJpaConfiguration(DataSourceManager dataSources) {
this.dataSources = dataSources;
}
protected abstract String persistenceUnitName();
protected abstract Class<?> packageEntityClass();
protected abstract DataSource useDataSource(DataSourceManager dataSources);
public abstract LocalContainerEntityManagerFactoryBean entityManagerFactoryBean();
public abstract PlatformTransactionManager transactionManagerBean();
public abstract LocalContainerEntityManagerFactoryBean getEntityManagerFactoryBean();
protected LocalContainerEntityManagerFactoryBean buildEntityManagerFactory() {
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setDataSource(useDataSource(dataSources));
em.setPersistenceUnitName(persistenceUnitName() + "PU");
em.setBeanName(persistenceUnitName() + "EntityManager");
em.setPackagesToScan(packageEntityClass().getPackage().getName());
em.setJpaPropertyMap(persistenceProperties());
em.setJpaVendorAdapter(jpaVendorAdapter());
return em;
}
protected Map<String, String> persistenceProperties() {
Map<String, String> properties = new HashMap<>();
properties.put("hibernate.hbm2ddl.auto", "validate");
return properties;
}
protected JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
adapter.setShowSql(false);
adapter.setGenerateDdl(false);
return adapter;
}
protected PlatformTransactionManager buildTransactionManager() {
LocalContainerEntityManagerFactoryBean emfBean = getEntityManagerFactoryBean();
EntityManagerFactory emf = emfBean.getObject();
return new JpaTransactionManager(emf);
}
}
An implementation of the configuration, located in the domain A package, looks like this:
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
basePackages = "com.mycompany.schema.domain_A",
entityManagerFactoryRef = "AEntityManagerFactory",
transactionManagerRef = "ATransactionManager")
@EntityScan(basePackages = "com.mycompany.schema.domain_A")
@DependsOn("flywayMigrationInitializer")
public class AJpaConfiguration extends AbstractJpaConfiguration {
@Autowired
public AJpaConfiguration(DataSourceManager dataSources) {
super(dataSources);
}
@Override
protected Class<?> packageEntityClass() {
return getClass(); // This class is located in the entity class package
}
@Override
protected String persistenceUnitName() {
return "a";
}
@Override
protected DataSource useDataSource(DataSourceManager dataSources) {
return dataSources.domainADataSource();
}
@Bean("aEntityManagerFactory")
@Override
public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean() {
return buildEntityManagerFactory();
}
@Bean("aTransactionManager")
@Override
public PlatformTransactionManager transactionManagerBean() {
return buildTransactionManager();
}
@Override
public LocalContainerEntityManagerFactoryBean getEntityManagerFactoryBean() {
return entityManagerFactoryBean();
}
}
Then, the actual repository is defined as a JpaRepository:
@Repository
public interface ARepository extends JpaRepository<AEntity, Long> {
}
This seems to work, according to the application logs:
2018-12-14 09:45:02.997 INFO 13867 --- [ restartedMain] .s.d.r.c.RepositoryConfigurationDelegate : Multiple Spring Data modules found, entering strict repository configuration mode!
2018-12-14 09:45:02.997 INFO 13867 --- [ restartedMain] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data repositories in DEFAULT mode.
2018-12-14 09:45:03.012 INFO 13867 --- [ restartedMain] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 9ms. Found 1 repository interface.
2018-12-14 09:45:03.029 INFO 13867 --- [ restartedMain] .s.d.r.c.RepositoryConfigurationDelegate : Multiple Spring Data modules found, entering strict repository configuration mode!
2018-12-14 09:45:03.029 INFO 13867 --- [ restartedMain] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data repositories in DEFAULT mode.
2018-12-14 09:45:03.085 INFO 13867 --- [ restartedMain] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 56ms. Found 1 repository interface.
And after that, and the successful Flyway migration, the Persistence Units are started:
2018-12-14 09:45:06.459 INFO 13867 --- [ restartedMain] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [
name: aPU
...]
2018-12-14 09:45:06.544 INFO 13867 --- [ restartedMain] org.hibernate.Version : HHH000412: Hibernate Core {5.3.7.Final}
2018-12-14 09:45:06.546 INFO 13867 --- [ restartedMain] org.hibernate.cfg.Environment : HHH000206: hibernate.properties not found
2018-12-14 09:45:06.746 INFO 13867 --- [ restartedMain] o.hibernate.annotations.common.Version : HCANN000001: Hibernate Commons Annotations {5.0.4.Final}
2018-12-14 09:45:06.922 INFO 13867 --- [ restartedMain] org.hibernate.dialect.Dialect : HHH000400: Using dialect: org.hibernate.dialect.MySQL57Dialect
2018-12-14 09:45:07.976 INFO 13867 --- [ restartedMain] o.h.h.i.QueryTranslatorFactoryInitiator : HHH000397: Using ASTQueryTranslatorFactory
2018-12-14 09:45:08.066 INFO 13867 --- [ restartedMain] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'aPU'
But, when I try to autowire the repository into a service:
@Service
public class MyService {
private final ARepository repository;
@Autowired
public MyService(ARepository repository) {
this.repository = repository;
}
// ...
}
This is the error message I recieve in the logs:
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'MyService' defined in URL [jar:file:...]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.mycompany.schema.domain_A.repository.ARepository' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:767) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:218) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1308) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1154) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:538) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:498) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:320) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:318) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:273) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1239) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1166) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:855) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:758) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
... 106 common frames omitted
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.mycompany.schema.domain_A.repository.ARepository' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:1646) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1205) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1166) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:855) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:758) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
... 120 common frames omitted
I hope the information provided can help solve this problem.
If not, I'll be happy to provide additional information.
Thank you for your time, and have a nice day
- Alexander
--- Edit ---
I put two breakpoints into the configuration classes, and got some new information:
The method for creating the LocalContainerEntityManagerFactoryBean
is called, but the application stops due to the missing bean before the PlatformTransactionManager
bean method is called.
What I overlooked was that the Service in question, which requires the JpaRepository subtype, is implementing the Spring Security UserDetailsManager
interface.
It seems like the Spring Security system is trying to instantiate the UserDetailsManager Service before the JpaRepository Beans picked up by @EnableJpaRepositories
can be created.
Is there any solution for that?
--- Edit 2 ---
I tried to @Import
the AJpaConfiguration class, which didn't change anything.
However, looking closer at the bean instantiation logs I found the following messages:
2018-12-14 11:52:08.395 DEBUG 22417 --- [ restartedMain] o.s.b.f.s.DefaultListableBeanFactory : Creating shared instance of singleton bean 'aEntityManagerFactory'
2018-12-14 11:52:08.395 DEBUG 22417 --- [ restartedMain] o.s.b.f.s.DefaultListableBeanFactory : Creating shared instance of singleton bean 'aJpaConfig'
So Spring Boot is picking up the @Bean annotation for the LocalContainerEntityManagerFactoryBean
, but neither creating EntityManagerFactory instances nor picking up the @Bean annotation for the PlatformTransactionManager
.
--- Edit 3 ---
I set the @Autowired option required = false
, and now it's picking up the beans and instantiating the repositories - but only after instantiating the services that require them, and thus not injecting them.
2018-12-14 13:25:30.484 DEBUG 31932 --- [ restartedMain] o.s.b.f.s.DefaultListableBeanFactory : Creating shared instance of singleton bean 'aEntityManager'
2018-12-14 13:25:33.035 DEBUG 31932 --- [ restartedMain] o.s.b.f.s.DefaultListableBeanFactory : Creating shared instance of singleton bean 'aTransactionManager'
2018-12-14 13:25:34.852 DEBUG 31932 --- [ restartedMain] o.s.b.f.s.DefaultListableBeanFactory : Creating shared instance of singleton bean 'aRepository'
How can I create the beans before everything else? That would seem to solve the problem.
--- Edit 4 ---
Thanks to the tip by @RobertNiestroj, I added @Lazy
to the @Autowired
annotations, which causes them to be initialized after then repositories.
But now, a different error appears:
java.lang.IllegalArgumentException: interface com.mycompany.schema.domain_A.repository.ARepository is not visible from class loader
I should also mention this Application is built out of Maven Modules: - Schema module (JPA configuration, repositories, and entity classes) - Core module (Application configuration and property classes) - [Other modules using Core and Schema] - Main module (Spring-Boot Application class)
Upvotes: 4
Views: 10566
Reputation: 1
Why not use Spring Profiles instead of configuring both database connections in a single application.yml file? You can create separate application.yml files for each profile and assign each profile to a different database connection.
By using profiles, you can easily manage different environments (e.g., development, testing, production) with their respective configurations. For example:
Create separate configuration files for each environment:
application-dev.yml for development application-prod.yml for production Activate the profile you need in your main application.yml or as a command-line argument when running your application.
Example: application.yml:
yaml spring: profiles: active: dev application-dev.yml:
yaml spring: datasource: url: jdbc:postgresql://localhost:5432/dev_db username: dev_user password: dev_password application-prod.yml:
yaml spring: datasource: url: jdbc:postgresql://localhost:5432/prod_db username: prod_user password: prod_password This way, you can cleanly separate your database configurations for each profile without cluttering a single file.
Upvotes: -1
Reputation: 98
Setting up multiple data sources in a Spring Boot application using JPA (Java Persistence API) and JPARepository requires defining multiple DataSource beans, EntityManagerFactory, and TransactionManager beans.
Step 1: Define Database Properties
spring:
datasource:
primary:
url: jdbc:mysql://localhost:3306/primary_db
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
secondary:
url: jdbc:mysql://localhost:3306/secondary_db
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
jpa: hibernate: ddl-auto: update show-sql: true`
Step 2: Define Entity Classes Each database will have its own set of entities. a) User b) Order
Step 3: Configure DataSources
@Configuration
@EnableTransactionManagement
public class DataSourceConfig {
private final JpaProperties jpaProperties;
@Autowired
public DataSourceConfig(JpaProperties jpaProperties) {
this.jpaProperties = jpaProperties;
}
public DataSource createDataSource(String prefix) {
return DataSourceBuilder.create()
.url(jpaProperties.getProperties().get(prefix + ".url"))
.username(jpaProperties.getProperties().get(prefix + ".username"))
.password(jpaProperties.getProperties().get(prefix + ".password"))
.driverClassName(jpaProperties.getProperties().get(prefix + ".driver-class-name"))
.build();
}
public LocalContainerEntityManagerFactoryBean createEntityManagerFactory(
EntityManagerFactoryBuilder builder, DataSource dataSource, String packageToScan, String unitName) {
return builder
.dataSource(dataSource)
.packages(packageToScan)
.persistenceUnit(unitName)
.build();
}
public PlatformTransactionManager createTransactionManager(EntityManagerFactory entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory);
}
}
a) Primary datasorce:
@Configuration
@EnableJpaRepositories(
basePackages = "com.example.repository.primary",
entityManagerFactoryRef = "primaryEntityManagerFactory",
transactionManagerRef = "primaryTransactionManager"
)
public class PrimaryDataSourceConfig {
private final DataSourceConfig dataSourceConfig;
@Autowired
public PrimaryDataSourceConfig(DataSourceConfig dataSourceConfig) {
this.dataSourceConfig = dataSourceConfig;
}
@Primary
@Bean(name = "primaryDataSource")
@ConfigurationProperties(prefix = "spring.datasource.primary")
public DataSource dataSource() {
return dataSourceConfig.createDataSource("spring.datasource.primary");
}
@Primary
@Bean(name = "primaryEntityManagerFactory")
public LocalContainerEntityManagerFactoryBean entityManagerFactory(
EntityManagerFactoryBuilder builder,
@Qualifier("primaryDataSource") DataSource dataSource) {
return dataSourceConfig.createEntityManagerFactory(builder,
dataSource, "com.example.entity.primary", "primary");
}
@Primary
@Bean(name = "primaryTransactionManager")
public PlatformTransactionManager transactionManager(
@Qualifier("primaryEntityManagerFactory") EntityManagerFactory
entityManagerFactory) {
return
dataSourceConfig.createTransactionManager(entityManagerFactory);
}
}
b) secondary datasource:
@Configuration
@EnableJpaRepositories(
basePackages = "com.example.repository.secondary",
entityManagerFactoryRef = "secondaryEntityManagerFactory",
transactionManagerRef = "secondaryTransactionManager"
)
public class SecondaryDataSourceConfig {
private final DataSourceConfig dataSourceConfig;
@Autowired
public SecondaryDataSourceConfig(DataSourceConfig dataSourceConfig) {
this.dataSourceConfig = dataSourceConfig;
}
@Bean(name = "secondaryDataSource")
@ConfigurationProperties(prefix = "spring.datasource.secondary")
public DataSource dataSource() {
return
dataSourceConfig.createDataSource("spring.datasource.secondary");
}
@Bean(name = "secondaryEntityManagerFactory")
public LocalContainerEntityManagerFactoryBean entityManagerFactory(
EntityManagerFactoryBuilder builder,
@Qualifier("secondaryDataSource") DataSource dataSource) {
return dataSourceConfig.createEntityManagerFactory(builder,
dataSource, "com.example.entity.secondary", "secondary");
}
@Bean(name = "secondaryTransactionManager")
public PlatformTransactionManager transactionManager(
@Qualifier("secondaryEntityManagerFactory") EntityManagerFactory
entityManagerFactory) {
return
dataSourceConfig.createTransactionManager(entityManagerFactory);
}
}
step-4 Define JPA Repositories
a) primary repo:
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}
b) secondary repo:
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
}
This should work as you intended to work. Happy coding.
Upvotes: 0
Reputation:
Have you tried @Repository(name = "repository")? IIRC, by not providing a name, the component gets created with a name matching the class, in this case "aRepository". And thus when autowiring the repository variable, there's no component named "repository", resulting in the no qualifying bean exception.
Upvotes: 0
Reputation: 1528
Please try autowiring as below and check
@Service
public class MyService {
@Autowired
private ARepository repository;
// ...
}
Upvotes: -1