Spring boot multiple data sources - Connectivity Error Handling - spring-boot

I'm working with multiple data sources (Oracle and SQL-Server) in spring boot rest application. In this application, I have more than 25+ end-points exist to process client requests. But when one of the databases is down like Oracle or SQL-server is not available for some reason, my application is unable to start the server.
Looked couple examples on google and stack overflow but they're different what I'm looking for...
package com.foobar;
#Configuration
#EnableTransactionManagement
#EnableJpaRepositories
(
entityManagerFactoryRef = "entityManagerFactory",
basePackages = { "com.foobar.foo.repo" }
)
public class FooDbConfig
{
#Primary
#Bean(name = "dataSource")
#ConfigurationProperties(prefix = "spring.datasource")
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
#Primary
#Bean(name = "entityManagerFactory")
public LocalContainerEntityManagerFactoryBean
entityManagerFactory(
EntityManagerFactoryBuilder builder,
#Qualifier("dataSource") DataSource dataSource
) {
return builder
.dataSource(dataSource)
.packages("com.foobar.foo.domain")
.persistenceUnit("foo")
.build();
}
#Primary
#Bean(name = "transactionManager")
public PlatformTransactionManager transactionManager(
#Qualifier("entityManagerFactory") EntityManagerFactory
entityManagerFactory)
{
return new JpaTransactionManager(entityManagerFactory);
}
}
same configuration for 2nd data-source but with different properties.
I'm using below example as a base code reference to implement my requirements
Example link
I'm looking for a solution if one DB server is available out of N application should start and process client requests and whenever 2nd DB server is available then it connects automatically and processes other endpoints requests

I've created a solution recently for multitenancy with datasources and using liquibase, but if not use the liquibase, just remove that works too!
Example of application.yml
spring:
dataSources:
- tenantId: db1
url: jdbc:postgresql://localhost:5432/db1
username: postgres
password: 123456
driver-class-name: org.postgresql.Driver
liquibase:
enabled: true
default-schema: public
change-log: classpath:db/master/changelog/db.changelog-master.yaml
- tenantId: db2
url: jdbc:postgresql://localhost:5432/db2
username: postgres
password: 123456
driver-class-name: org.postgresql.Driver
- tenantId: db3
url: jdbc:postgresql://localhost:5432/db3
username: postgres
password: 123456
driver-class-name: org.postgresql.Driver
DataSourceConfiguration
#Configuration
#EnableTransactionManagement
#EntityScan(basePackages = { "br.com.dijalmasilva.springbootmultitenancyliquibase" })
#EnableJpaRepositories(basePackages = { "br.com.dijalmasilva.springbootmultitenancyliquibase" })
public class DataSourceConfiguration {
#Bean(name = "dataSources")
#Primary
public Map<Object, Object> getDataSources(DataSourceProperties dataSourceProperties) {
return dataSourceProperties.getDataSources().stream().map(dataSourceProperty -> {
DataSource dataSource = DataSourceBuilder.create()
.url(dataSourceProperty.getUrl())
.username(dataSourceProperty.getUsername())
.password(dataSourceProperty.getPassword())
.driverClassName(dataSourceProperty.getDriverClassName())
.build();
return new TenantIdDataSource(dataSourceProperty.getTenantId(), dataSource);
}).collect(Collectors.toMap(TenantIdDataSource::getTenantId, TenantIdDataSource::getDataSource));
}
#Bean(name = "tenantRoutingDataSource")
#DependsOn("dataSources")
public DataSource dataSource(Map<Object, Object> dataSources) {
AbstractRoutingDataSource tenantRoutingDataSource = new TenantRoutingDataSource();
tenantRoutingDataSource.setTargetDataSources(dataSources);
tenantRoutingDataSource.setDefaultTargetDataSource(dataSources.get("db1"));
tenantRoutingDataSource.afterPropertiesSet();
return tenantRoutingDataSource;
}
#Data
#AllArgsConstructor
private class TenantIdDataSource {
private Object tenantId;
private Object dataSource;
}
}
TenantRoutingDataSource
public class TenantRoutingDataSource extends AbstractRoutingDataSource {
#Override
protected Object determineCurrentLookupKey() {
return TenantContext.getCurrentTenant();
}
}
DataSourceProperties
#Data
#Component
#ConfigurationProperties(prefix = "spring")
public class DataSourceProperties {
private List<DataSourceProperty> dataSources = new ArrayList<>();
}
DataSourceProperty
#Data
public class DataSourceProperty {
private String tenantId;
private String url;
private String username;
private String password;
private String driverClassName;
private LiquibaseProperties liquibase;
}
See the complete code, maybe help you!
Link of project: https://github.com/dijalmasilva/spring-boot-multitenancy-datasource-liquibase

Related

One Database overrides the other when Primary Bean is defined on and vice versa

I have 2 JDBC Datasources defined in a Spring Boot application utilizing used in a Spring Batch job. However, after autowiring the datasources, only one gets used. The one used is the one annotated #Primary. If I place the annotation on the other JDBC datasource that gets used instead. In a nutshell only one of the JDBC datasources ever gets used. I use Lombok in some places but I'm unsure if that is playing a part.
Here are the datasources:
application.yml
symphony:
datasource:
driver-class-name: oracle.jdbc.OracleDriver
url: ...
type: com.zaxxer.hikari.HikariDataSource
username: <USR>
password: <PWD>
jndi-name: false
repo:
datasource:
driver-class-name: org.h2.Driver
url: jdbc:h2:mem:db;DB_CLOSE_DELAY=-1
username: sa
password: sa
jndi-name: false
Here is the first datasource:
#Configuration
public class RepoDbConfig {
#Bean
#ConfigurationProperties("repo.datasource")
public DataSourceProperties repoDataProperties() {
return new DataSourceProperties();
}
#Bean(name = "repoDataSource")
public DataSource dataSourcerepo() {
DataSource dataSource = repoDataProperties().initializeDataSourceBuilder().type(BasicDataSource.class)
.build();
return dataSource;
}
#Bean(name = "repoJdbcTemplate")
public JdbcTemplate repoJdbcTemplate(DataSource repoDataSource) {
return new JdbcTemplate(repoDataSource);
}
}
Here is the second datasource:
#Configuration
public class SymphonyDbConfig {
#Primary
#Bean
#ConfigurationProperties("symphony.datasource")
public DataSourceProperties symphonyDataSourceProperties() {
return new DataSourceProperties();
}
#Primary
#Bean(name = "symphonyDataSource")
public DataSource dataSourcesymphony() {
HikariDataSource dataSource = symphonyDataSourceProperties().initializeDataSourceBuilder().type(HikariDataSource.class)
.build();
return dataSource;
}
#Primary
#Bean(name = "symphonyJdbcTemplate")
public JdbcTemplate symphonyJdbcTemplate(DataSource symphonyDataSource) {
return new JdbcTemplate(symphonyDataSource);
}
}
The JobRepository beans are configured like this:
#Configuration
#RequiredArgsConstructor
public class JobRepositoryConfig {
final #Qualifier("repoDataSource")
DataSource repoDataSource;
#Bean("repoTransactionManager")
AbstractPlatformTransactionManager repoTransactionManager() {
return new ResourcelessTransactionManager();
}
#Bean("repoJobRepository")
public JobRepository repoJobRepository(DataSource repoDataSource) throws Exception {
JobRepositoryFactoryBean jobRepositoryFactoryBean = new JobRepositoryFactoryBean();
jobRepositoryFactoryBean.setDataSource(repoDataSource);
jobRepositoryFactoryBean.setTransactionManager(repoTransactionManager());
jobRepositoryFactoryBean.setDatabaseType(DatabaseType.H2.getProductName());
return jobRepositoryFactoryBean.getObject();
}
#Bean("repoAppJobLauncher")
public JobLauncher careLocationAppJobLauncher(JobRepository repoJobRepository) {
SimpleJobLauncher simpleJobLauncher = new SimpleJobLauncher();
simpleJobLauncher.setJobRepository(repoJobRepository);
return simpleJobLauncher;
}
}
Finally the Batch Job beans used for the Job are configured here: The only part not shown is the launching of the job. All the required beans used are shown here:
#Configuration
#EnableBatchProcessing
#EnableScheduling
#RequiredArgsConstructor
#Slf4j
public class CellBatchConfig {
private final JobBuilderFactory jobBuilderFactory;
#Qualifier("repoAppJobLauncher")
private final JobLauncher repoAppJobLauncher;
private final StepBuilderFactory stepBuilderFactory;
#Value("${chunk-size}")
private int chunkSize;
#Qualifier("symphonyDataSource")
final DataSource symphonyDataSource;
#Qualifier("repoDataSource")
final DataSource symphonyDataSource;
#Bean
public JdbcPagingItemReader<CenterDto> cellItemReader(PagingQueryProvider pagingQueryProvider) {
return new JdbcPagingItemReaderBuilder<CenterDto>()
.name("cellItemReader")
.dataSource(symphonyDataSource)
.queryProvider(pagingQueryProvider)
.pageSize(chunkSize)
.rowMapper(new CellRowMapper())
.build();
}
#Bean
public PagingQueryProvider pagingQueryProvider() {
OraclePagingQueryProvider pagingQueryProvider = new OraclePagingQueryProvider();
final Map<String, Order> sortKeys = new HashMap<>();
sortKeys.put("ID", Order.ASCENDING);
pagingQueryProvider.setSortKeys(sortKeys);
pagingQueryProvider.setSelectClause(" ID, CELL_NO, MAT_VO ");
pagingQueryProvider.setFromClause(" from pvc.cells");
return pagingQueryProvider;
}
.......
}
The error results from only one of the datasources being used. That results that being used to query the Spring Batch job repository resulting in it failing: Here is the key portion of the stacktrace. It is trying to use the oracle datasource to query for the JobRespository resources and fails as a result:
Caused by: org.springframework.jdbc.BadSqlGrammarException:
PreparedStatementCallback; bad SQL grammar [SELECT JOB_INSTANCE_ID, JOB_NAME from
BATCH_JOB_INSTANCE
where JOB_NAME = ? and JOB_KEY = ?]; nested exception is
java.sql.SQLSyntaxErrorException: ORA-00942: table or view does not exist
In the class JobRepositoryConfig:
In the bean:
#Bean("symphonyJobRepository")
public JobRepository symphonyJobRepository(DataSource repoDataSource) throws Exception {
JobRepositoryFactoryBean jobRepositoryFactoryBean = new JobRepositoryFactoryBean();
jobRepositoryFactoryBean.setDataSource(repoDataSource);
jobRepositoryFactoryBean.setTransactionManager(repoTransactionManager());
jobRepositoryFactoryBean.setDatabaseType(DatabaseType.H2.getProductName());
return jobRepositoryFactoryBean.getObject();
}
You didn't use the variable:
final #Qualifier("repoDataSource") DataSource repoDataSource;
So Spring uses a DataSource object which is annotated with #Primary annotation
I fixed it by making one bean the primary and also adding the qualifiers on the specific beans which had been missing, an omission on my part. For example, here I added the #Qualifier:
#Primary
#Bean(name = "symphonyDataSource")
#Qualifier("symphonyDataSource") // This was missing
public DataSource dataSourcesymphony() {
HikariDataSource dataSource = symphonyDataSourceProperties().initializeDataSourceBuilder().type(HikariDataSource.class)
.build();
return dataSource;
}

Spring Boot Multitenancy - Hibernate - Use ddl-auto to update all schemas when entity structure changes

I am new to Spring Boot and trying to implement multi-tenancy architecture using Spring boot 2, hibernate and flyway. I was referring to tutorial https://reflectoring.io/flyway-spring-boot-multitenancy/ to understand the concepts and was able to implement the architecture as mentioned.
However, if I add a new field entity classes, everything breaks because hibernate does not create new fields in tenant databases. From reading theory and stackoverflow questions, I understand that flyway is to solve this problem. However, i am not able to make it work.
Can somebody tell me where I am going wrong. My requirement is - When I add a new field to entity class, all tables in all tenant databases should get updated with that field. Below is the code
Application Properties
spring:
jpa:
database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
flyway:
enabled: false
tenants:
datasources:
vw:
jdbcUrl: jdbc:mysql://localhost:3306/vw
driverClassName: com.mysql.jdbc.Driver
username: vikky
password: Test#123
bmw:
jdbcUrl: jdbc:mysql://localhost:3306/bmw
driverClassName: com.mysql.jdbc.Driver
username: vikky
password: Test#123
Datasource Configuration
#Configuration
public class DataSourceConfiguration {
private final DataSourceProperties dataSourceProperties;
public DataSourceConfiguration(DataSourceProperties dataSourceProperties) {
this.dataSourceProperties = dataSourceProperties;
}
#Bean
public DataSource dataSource() {
TenantRoutingDataSource customDataSource = new TenantRoutingDataSource();
customDataSource.setTargetDataSources(dataSourceProperties.getDatasources());
return customDataSource;
}
#PostConstruct
public void migrate() {
dataSourceProperties
.getDatasources()
.values()
.stream()
.map(dataSource -> (DataSource) dataSource)
.forEach(this::migrate);
}
private void migrate(DataSource dataSource) {
Flyway flyway = Flyway.configure().dataSource(dataSource).load();
flyway.migrate();
}
}
DataSource properties
#Component
#ConfigurationProperties(prefix = "tenants")
public class DataSourceProperties {
private Map<Object, Object> datasources = new LinkedHashMap<>();
public Map<Object, Object> getDatasources() {
return datasources;
}
public void setDatasources(Map<String, Map<String, String>> datasources) {
datasources
.forEach((key, value) -> this.datasources.put(key, convert(value)));
}
public DataSource convert(Map<String, String> source) {
return DataSourceBuilder.create()
.url(source.get("jdbcUrl"))
.driverClassName(source.get("driverClassName"))
.username(source.get("username"))
.password(source.get("password"))
.build();
}
}
Tenant Routing Data Source
public class TenantRoutingDataSource extends AbstractRoutingDataSource {
#Override
protected Object determineCurrentLookupKey() {
return ThreadTenantStorage.getTenantId();
}
}
Header Interceptor
#Component
public class HeaderTenantInterceptor implements WebRequestInterceptor {
public static final String TENANT_HEADER = "X-tenant";
#Override
public void preHandle(WebRequest request) throws Exception {
ThreadTenantStorage.setTenantId(request.getHeader(TENANT_HEADER));
}
#Override
public void postHandle(WebRequest request, ModelMap model) throws Exception {
ThreadTenantStorage.clear();
}
#Override
public void afterCompletion(WebRequest request, Exception ex) throws Exception {
}
}
There are other classes as well like Web Configuration, controllers etc. but I don't they are required to be posted here.
After lot of research, I understood that flyway is required only in case of production, where we do not want to update table definition using ddl-auto=true. Since that was not the case with me, I added below configuration to update all schemas according to entity structure
#Configuration
public class AutoDDLConfig
{
#Value("${spring.datasource.username}")
private String username;
#Value("${spring.datasource.password}")
private String password;
#Value("${schemas.list}")
private String schemasList;
#Bean
public void bb()
{
if (StringUtils.isBlank(schemasList))
{
return;
}
String[] tenants = schemasList.split(",");
for (String tenant : tenants)
{
tenant = tenant.trim();
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver"); // Change here to MySql Driver
dataSource.setSchema(tenant);
dataSource.setUrl("jdbc:mysql://localhost/" + tenant
+ "?autoReconnect=true&characterEncoding=utf8&useSSL=false&useTimezone=true&serverTimezone=Asia/Kolkata&useLegacyDatetimeCode=false&allowPublicKeyRetrieval=true");
dataSource.setUsername(username);
dataSource.setPassword(password);
LocalContainerEntityManagerFactoryBean emfBean = new LocalContainerEntityManagerFactoryBean();
emfBean.setDataSource(dataSource);
emfBean.setPackagesToScan("com"); // Here mention JPA entity path / u can leave it scans all packages
emfBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
emfBean.setPersistenceProviderClass(HibernatePersistenceProvider.class);
Map<String, Object> properties = new HashMap<>();
properties.put("hibernate.hbm2ddl.auto", "update");
properties.put("hibernate.default_schema", tenant);
emfBean.setJpaPropertyMap(properties);
emfBean.setPersistenceUnitName(dataSource.toString());
emfBean.afterPropertiesSet();
}
}
}

Spring Boot - connecting to two schema per tenant

I am creating a Spring boot Multimodule application in which one of the module has unique requirement. The module should be able to connect to one fixed schema say S1 and there are two other schema S2 and S3.
Depending upon the region it will connect to S1 and S2 or S1 and S3.
Will I have to follow the multi tenancy approach here ?
If yes how do I use multi tenancy to connect to two schema for a particular request ?
As per my understanding you want to connect to different DB's based on country. I have similar kind of issue in my project.
My requirement is when there is a json request i need to fetch country code from that and based on that i'll connect to that country specific DB. Below is my code. Hope this helps you.
application.properties
spring.main.web-application-type=none
#spring.main.allow-bean-definition-overriding=true
#au DB connection
spring.datasource.au.url=
spring.datasource.au.username=
spring.datasource.au.password=
#cn DB connection
spring.datasource.cn.url=
spring.datasource.cn.username=
spring.datasource.cn.password=
spring.jpa.show-sql=true
spring.jpa.database=default
spring.jpa.hibernate.ddl-auto=none
spring.jpa.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyHbmImpl
spring.jpa.hibernate.naming.physical-strategy=org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
DB configuration
For Au :
Here i'm using #Primary annotation for AU.
#Configuration
#EnableJpaRepositories(entityManagerFactoryRef = "auEntityManagerFactory", transactionManagerRef = "auTransactionManager", basePackages = "com.dbs.multiDb.au.repository")
#EnableTransactionManagement
public class auDsConfig {
Logger logger = LogManager.getLogger(this.getClass());
#Autowired
Environment env;
#Bean(name = "auEntityManagerFactory")
#Primary
public LocalContainerEntityManagerFactoryBean cnEntityManagerFactory(final EntityManagerFactoryBuilder builder,
final #Qualifier("au-db") DataSource dataSource) {
return builder.dataSource(dataSource).packages("com.dbs.multiDb.domain").persistenceUnit("auDb")
.properties(singletonMap("hibernate.hbm2ddl.auto", "none")).build();
}
#Bean(name = "auTransactionManager")
#Primary
public PlatformTransactionManager auTransactionManager(
#Qualifier("auEntityManagerFactory") EntityManagerFactory cnEntityManagerFactory) {
return new JpaTransactionManager(cnEntityManagerFactory);
}
#SuppressWarnings("unused")
#Bean(name = "au-db")
#Primary
#ConfigurationProperties(prefix = "spring.datasource.au")
public DataSource auDataSource() {
DriverManagerDataSource dbFinal = new DriverManagerDataSource();
dbFinal.setDriverClassName(env.getProperty("jdbc.driverClassName"));
dbFinal.setUrl(env.getProperty("spring.datasource.au.url"));
dbFinal.setUsername(env.getProperty("spring.datasource.au.username"));
dbFinal.setPassword(env.getProperty("spring.datasource.au.password"));
return dbFinal;
}
}
CN DB config :
#Configuration
#EnableJpaRepositories(entityManagerFactoryRef = "cnEntityManagerFactory", transactionManagerRef = "cnTransactionManager", basePackages = "com.dbs.multiDb.cn.repository")
#EnableTransactionManagement
public class cnDsConfig {
#Bean(name = "cnEntityManagerFactory")
public LocalContainerEntityManagerFactoryBean cnEntityManagerFactory(final EntityManagerFactoryBuilder builder,
final #Qualifier("cn-db") DataSource dataSource) {
return builder.dataSource(dataSource).packages("com.dbs.multiDb.domain").persistenceUnit("cnDb")
.properties(singletonMap("hibernate.hbm2ddl.auto", "none")).build();
}
#Bean(name = "cnTransactionManager")
public PlatformTransactionManager cnTransactionManager(
#Qualifier("cnEntityManagerFactory") EntityManagerFactory cnEntityManagerFactory) {
return new JpaTransactionManager(cnEntityManagerFactory);
}
#Bean(name = "cn-db")
#ConfigurationProperties(prefix = "spring.datasource.cn")
public DataSource cnDataSource() {
DriverManagerDataSource dbFinal = new DriverManagerDataSource();
dbFinal.setDriverClassName(env.getProperty("jdbc.driverClassName"));
dbFinal.setUrl(env.getProperty("spring.datasource.cn.url"));
dbFinal.setUsername(env.getProperty("spring.datasource.cn.username"));
dbFinal.setPassword(env.getProperty("spring.datasource.cn.password"));
return dbFinal;
}
}
Created REPO's for both countries :
AU Repo :
#Repository
public interface AURepository extends CrudRepository<BeanName,String>{
}
CN Repo :
#Repository
public interface CNRepository extends CrudRepository<BeanName,String>{
}
Service class :
#Service
public class ServiceClass{
private AURepository auTxn;
private CNRepository cnTxn;
#Autowired
public ServiceClass(AURepository auTxn, CNRepository cnTxn) {
super();
this.auTxn = auTxn;
this.cnTxn = cnTxn;
}
public List<Bean> getData(String input, String country) {
List<Bean> data= new ArrayList<Bean>();
//If this is AU then it will connect to AU DB and retrieve data.
if (country.equals("AU")) {
data= auTxn.findall();
}
//If this is CN then it will connect to AU DB and retrieve data.
else if (country.equals("CN")) {
data= cnTxn.findall();
}
return data;
}
}

Spring data - If primary datasource fails the second does not come up

I am using Spring boot and Spring data and I want to use primarily a MySQL datasource but if fails to connect go to a H2 datasource.
So far, I do the change just moving the #Primary in the configurations, but if I put the #Primary in the MySQL (main data source) and stop the MySQL server in my pc, the other bean does not come up... What do I need?
application.yml:
# Main properties
spring:
application:
name: app
jpa:
database: default
show-sql: false
hibernate:
ddl-auto: update
properties:
hibernate:
format_sql: false
current_session_context_class: org.springframework.orm.hibernate5.SpringSessionContext
# Main database: MySQL
main.datasource:
url: jdbc:mysql://localhost:3306/app?useSSL=false
driver-class-name: com.mysql.jdbc.Driver
username: sa
password: sa
# Backup database: H2
backup.datasource:
url: jdbc:h2:${project.directory}/app;DB_CLOSE_ON_EXIT=FALSE
driver-class-name: org.h2.Driver
username: sa
password: sa
Main data source
#Configuration
#EnableTransactionManagement
#EnableJpaRepositories("org.app")
#EntityScan("org.app")
public class MainDataSourceConfig {
#Primary
#Bean(name = "mainDataSource")
#ConfigurationProperties(prefix = "main.datasource")
public DataSource mainDataSource() {
return DataSourceBuilder.create().build();
}
}
Backup data source:
#Configuration
#EnableTransactionManagement
#EnableJpaRepositories("org.app")
#EntityScan("org.app")
public class BackupDataSourceConfig {
#Bean(name = "backupDataSource")
#ConfigurationProperties(prefix = "backup.datasource")
public DataSource backupDataSource() {
return DataSourceBuilder.create().build();
}
}
Thanks!
I figure out how to do it. Hope this can help anyone:
#Configuration
#EnableTransactionManagement
#EnableJpaRepositories("org.app")
#EntityScan("org.app")
public class DataSourceConfig {
private static final String USERNAME = "sa";
private static final String PASSWORD = "sa";
#Bean
#Primary
public DataSource dataSource() {
DataSource dataSource;
try {
dataSource = getMainDataSource();
dataSource.getConnection().isValid(500);
} catch (Exception e) {
log.error("Main database not valid.", e);
dataSource =getBackupDataSource();
}
return dataSource;
}
private DataSource getMainDataSource() {
return DataSourceBuilder.create()
.driverClassName("com.mysql.jdbc.Driver")
.username(USERNAME)
.password(PASSWORD)
.url("jdbc:mysql://localhost:3306/app?useSSL=false")
.build();
}
private DataSource getBackupDataSource() {
return DataSourceBuilder.create()
.driverClassName("org.h2.Driver")
.username(USERNAME)
.password(PASSWORD)
.url("jdbc:h2:/app;DB_CLOSE_ON_EXIT=FALSE")
.build();
}
}
Just on bean.

SpringBoot and SpringJDBC multiple datasources

I am trying to use two datasources with my SpringBoot application and can't get the second datasource to autowire. I have tried many things but this is the closest I have come:
My Yaml file:
spring:
first-datasource:
url: MyURLString1
username: User
password: Password
driver-class-name: oracle.jdbc.OracleDriver
second-datasource:
url: MyURLString2
username: User
password: Password
driver-class-name: oracle.jdbc.OracleDriver
My Application Class:
#SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
#Bean
#Primary
#ConfigurationProperties(prefix = "spring.first-datasource")
public DataSource firstDataSource() {
return DataSourceBuilder.create().build();
}
#Bean
#ConfigurationProperties(prefix = "spring.second-datasource")
public DataSource secondDataSource() {
return DataSourceBuilder.create().build();
}
}
And Finally my DAO:
#Repository
public class MyDao {
private static final String FIRST_SELECT = "select * from SomeTableInDB1";
private static final String SECOND_SELECT = "select * from AnotherTableInDB2";
#Autowired
private JdbcTemplate firstJdbcTemplate;
#Autowired
#Qualifier("secondDataSource")
private JdbcTemplate secondJdbcTemplate;
List<DB1Entity> getDB1Entity(Long id) {
return firstJdbcTemplate.query(FIRST_SELECT, new Object[] {id}, new BeanPropertyRowMapper(DB1Entity.class));
}
List<DB2Entity> getDB2Entity(Long id) {
return secondJdbcTemplate.query(SECOND_SELECT, new Object[] {id}, new BeanPropertyRowMapper(DB2Entity.class));
}
}
This is the closest I have come so far. I say it is closest because if I remove the #Qualifier then both of my dao methods actually work, assuming that the SECOND_SELECT statement is valid SQL for my DB1. Once I put in the #Qualifier for my non-primary datasouce then I get an autowire error because Spring is expecting a Datasouce object, not a JdbcTemplate object. That is weird to me as it does work with the primary datasource.
Here is my error:
Could not autowire field: private org.springframework.jdbc.core.JdbcTemplate org.my.classpath.secondJdbcTemplate; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [org.springframework.jdbc.core.JdbcTemplate] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {#org.springframework.beans.factory.annotation.Autowired(required=true), #org.springframework.beans.factory.annotation.Qualifier(value=secondDataSource)}
You create the bean of type DataSource, but try to Autowire JdbcTemplate which is a mismatch. Your probably should have something like this
private JdbcTemplate jdbcTemplate1;
private JdbcTemplate jdbcTemplate2;
#Autowired
#Qualifier("firstDataSource")
public void setDataSource(DataSource dataSource){
this.jdbcTemplate1=new JdbcTemplate(dataSource);
}
#Autowired
#Qualifier("secondDataSource")
public void setDataSource(DataSource dataSource){
this.jdbcTemplate2=new JdbcTemplate(dataSource);
}
Ideally, but not a mandate, one of the datasources should be marked PRIMARY for most of the default wiring via Annotations to work. Also, we need to create TransactionManagers for each of the datasources separately otherwise Spring would not know how to enforce Transactions. Following is a complete example of how this should be done
#Primary
#Bean(name = "dataSource")
#ConfigurationProperties(prefix="datasource.mysql")
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
#Primary
#Bean(name = "transactionManager")
public DataSourceTransactionManager transactionManager(#Qualifier("dataSource") DataSource dataSource) {
return new DataSourceTransactionManager();
}
#Bean(name = "postGresDataSource")
#ConfigurationProperties(prefix="datasource.postgres")
public DataSource postgresDataSource() {
return DataSourceBuilder.create().build();
}
#Bean(name = "postGresTransactionManager")
public DataSourceTransactionManager transactionManager(#Qualifier("postGresDataSource") DataSource dataSource) {
return new DataSourceTransactionManager();
}
#Transactional(transactionManager="postGresTransactionManager")
public void createCustomer(Customer cust) {
customerDAO.create(cust);
}
// specifying a transactionManager attribute is optional if we
// want to use the default transactionManager since we
// already marked one of the TM above with #Primary
#Transactional
public void createOrder(Order order) {
orderDAO.create(order);
}
Hope this helps.
Here also provide another 'not-working' situation that confused me for several days:
when configurating two data sources of same type in Springboot application, the #Qualifier doesn't work as expected to pick up right beans. It behaves like it's not recognized by Spring framework.
the reason is when using #SpringbootApplication annotation which contains #EnableAutoConfiguration annotation, which, in Springboot, will auto-configurate data sources it provides for users.
this would terribly affect #Qualifier's behavior.
In my case, following the #Aman Tuladhar answer, worked like that:
(Spring Boot 2.1.3.RELEASE)
#Configuration
public class JDBCConfig {
#Bean("first-datasource")
public JdbcTemplate paymentsJDBCTemplate(#Qualifier("first-db") DataSource paymentsDataSource){
return new JdbcTemplate(paymentsDataSource);
}
#Bean("second-datasource")
public JdbcTemplate parametersJDBCTemplate(#Qualifier("second-db") DataSource paramsDataSource){
return new JdbcTemplate(paramsDataSource);
}
#Bean("first-db")
public DataSource paymentsDataSource(Environment env) {
return buildDataSource(env, "first");
}
#Bean("second-db")
public DataSource paramsDataSource(Environment env) {
return buildDataSource(env, "second");
}
private DataSource buildDataSource(Environment env, String prop) {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName(env.getProperty("spring."+prop+"-datasource.driver-class-name"));
dataSource.setUrl(env.getProperty("spring."+prop+"-datasource.url"));
dataSource.setUsername(env.getProperty("spring."+prop+"-datasource.username"));
dataSource.setPassword(env.getProperty("spring."+prop+"-datasource.password"));
return dataSource;
}
}
... and using like that:
#Autowired #Qualifier("first-datasource")
private JdbcTemplate jdbcTemplate1;
#Autowired #Qualifier("second-datasource")
private JdbcTemplate jdbcTemplate2;
... application.yml:
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: ${DATABASE_1_URL}
username: ${DATABASE_1_USERNAME}
password: ${DATABASE_1_PASSWORD}
second-datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: ${DATABASE_2_URL}
username: ${DATABASE_2_USERNAME}
password: ${DATABASE_2_PASSWORD}

Resources