Spring Boot with two datasources: How to setup unique resource names? - java

As pointed out here, you can define two Datasources in Spring Boot in the following way:
#first db
spring.datasource.url = [url]
spring.datasource.username = [username]
spring.datasource.password = [password]
spring.datasource.driverClassName = oracle.jdbc.OracleDriver
#second db ...
spring.secondDatasource.url = [url]
spring.secondDatasource.username = [username]
spring.secondDatasource.password = [password]
spring.secondDatasource.driverClassName = oracle.jdbc.OracleDriver
#Bean
#Primary
#ConfigurationProperties(prefix="spring.datasource")
public DataSource primaryDataSource() {
return DataSourceBuilder.create().build();
}
#Bean
#ConfigurationProperties(prefix="spring.secondDatasource")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().build();
}
This seems to work fine. But when you are doing Atomikos XA-Transactions, each datasource has to has a unique resource name for the case that a recovery is necessary.
The Boot documentation defines a property for this:
spring.jta.atomikos.datasource.unique-resource-name=dataSource # The unique name used to identify the resource during recovery.
How do I provide a unique-resource-name for the primary and a different one for the secondary resource in Spring Boot?
Looks to me like XA-Transactions are supported in Boot, but only between JMS and DB ressources...

Related

setup alternate datasource in spring boot application causes liquibase to fail

Application has a default spring data source specified in application.yml
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
url: jdbc:oracle:thin:#localhost:1521:xe
username: system
password: oracle
hikari:
poolName: Hikari
auto-commit: false
I have added configuration options for a second data source, used for a completely difference (JDBCTemplate purpose).
faas20:
ds:
url: jdbc:oracle:thin:#tldb0147vm.group.net:1760:tdb
username: ...
password: ...
Then, I add two data sources, one named, and the other default. Without the default one, liquibase fails to start.
#Configuration
public class LegacyConfiguration {
#Bean(name = "faas20")
#ConfigurationProperties(prefix = "faas20.ds")
public DataSource legacyDataSource() {
return DataSourceBuilder
.create()
.build();
}
#Bean
public DataSource defaultDataSource() {
return DataSourceBuilder
.create()
.build();
}
}
Startup of the application fails though.
The application now cannot build the default EntityManagerFactory.
Why would that be affected?
Parameter 0 of constructor in impl.OrderServiceImpl required a bean named 'entityManagerFactory' that could not be found.
Consider defining a bean named 'entityManagerFactory' in your configuration.
Without the two data sources present the application and liquibase start up as they should.
edit
I am not clear on how to configure two separate data sources,
Default Data Source for JPA
Additional Data Source for use in JDBC (and potentially other JPA classes)

How to run two independent transaction managers for jpa and jdbc datasources

I'm developing a standalone application with following technology stack:
Spring Boot version 2.1.0.RELEASE
Oracle12c System with driver ojdbc6 (11.2.0.3)
Apache Camel
JPA for the main datasource
JDBC for a secondary datasource (read only)
The JPA datasource is the primary datasource where the application itself is connected to and write data to. The JDBC is an additional datasource to read data from a database with another purpose.
During runtime I encounter the following issue:
I poll/select a JPA Entity from the primary datasource and do some processing. This processing includes running a select query on the secondary datasource via a jdbc template. Now if the execution of the query throws an exception I am able to catch it and want to update a status field on the JPA Entity and write it to the datasource.
I've already read that Oracle tries to do a rollback if a SQLException occurrs. The issue is that my JPA datasource is unable to commit the changes to the Entity I do when the JDBC query fails.
It seems to me like the two datasources/transaction managers are not completely independent of each other and that an exception in the secondary datasource causes errors in the primary datasource during commiting changes.
Is this even possible? If yes, how can I configure two independent transaction managers?
EDIT:
I have already tried to annotate the respective methods and classes with #Transactional(noRollbackFor = Exception.class) but this does not solve the problem.
Here are the two Datasource configurations:
ApplicationDatasourceConfig (JPA)
#Configuration
#EnableJpaRepositories(basePackages = "foo.bar.repository.jpa",
entityManagerFactoryRef = "applicationEntityManagerFactory",
transactionManagerRef = "applicationTransactionManager")
#ConfigurationProperties(prefix = "spring.datasource.hikari")
#EnableTransactionManagement
public class ApplicationDatasourceConfig extends HikariConfig{
#Bean("applicationDatasource")
#Primary
public DataSource applicationDataSource(){
return new HikariDataSource(this);
}
#Bean("applicationDatasourceProperties")
#Primary
public DataSourceProperties dataSourceProperties(){
return new DataSourceProperties();
}
#Bean("applicationEntityManagerFactory")
public LocalContainerEntityManagerFactoryBean applicationEntityManagerFactory(EntityManagerFactoryBuilder builder,
#Qualifier("applicationDatasource") DataSource dataSource){
return builder
.dataSource(dataSource)
.packages("foo.bar.entity")
.build();
}
#Bean("applicationTransactionManager")
#Primary
public PlatformTransactionManager applicationTransactionManager(#Qualifier("applicationEntityManagerFactory")EntityManagerFactory entityManagerFactory){
return new JpaTransactionManager(entityManagerFactory);
}
}
SecondaryDatasourceConfig (JDBC)
#Configuration
#ConfigurationProperties(prefix = "secondary.datasource")
#EnableTransactionManagement
public class SecondaryDatasourceConfig {
#Bean("secondaryDatasource")
public DataSource secondaryDataSource(){
return secondaryDataSourceProperties().initializeDataSourceBuilder().build();
}
#Bean
public DataSourceProperties secondaryDataSourceProperties(){
return new DataSourceProperties();
}
#Bean("secondaryTransactionManager")
public PlatformTransactionManager secondaryTransactionManager(#Qualifier("secondaryDatasource") DataSource secondaryDataSource){
return new DataSourceTransactionManager(secondaryDataSource);
}
}

Connect multiple MySql db's to application

I want to connect multiple mysql db in my spring boot application. The thing is in my application, one of the db is used as an entity while from other db, I am fetching data in query form. So I want that whenever I write a custom query, it should take from one db while whenever I use repository methods, it should use another one.
Change your application.properties file as :
#first db
spring.datasource.url = [url]
spring.datasource.username = [username]
spring.datasource.password = [password]
spring.datasource.driverClassName = oracle.jdbc.OracleDriver
#second db ...
spring.secondDatasource.url = [url]
spring.secondDatasource.username = [username]
spring.secondDatasource.password = [password]
spring.secondDatasource.driverClassName = oracle.jdbc.OracleDriver
And Change your Configuration file i.e add following beans :
#Bean
#Primary
#ConfigurationProperties(prefix="spring.datasource")
public DataSource primaryDataSource() {
return DataSourceBuilder.create().build();
}
#Bean
#ConfigurationProperties(prefix="spring.secondDatasource")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().build();
}

Spring Boot integration test using multiple database types

In my test I need test with different databases (mysql, oracle, etc.) and I would like to know if it's possible with SpringRunner.
I'm using #SqlGroup and #Sql annotations, but I didn't discover how to indicate script files (sql) corresponding database.
Example:
#Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = "classpath:tenantBeforeTestRun.sql")
This annotation configures my test to execute the script to all database types, but this file didn't work on Oracle.
#Sql annotation lets you define a SqlConfig which contains a datasource bean name.
Then you can define many datasource beans, with possibly different drivers and refer them from different #Sql. This might be helpful: Spring Boot Multiple Datasource
#Sql(..., config = #SqlConfig(datasource = "db1", ...)
application.properties:
#first db
spring.db1.url = [url]
spring.db1.username = [username]
spring.db1.password = [password]
spring.db1.driverClassName = oracle.jdbc.OracleDriver
#second db ...
spring.secondDatasource.url = [url]
spring.secondDatasource.username = [username]
spring.secondDatasource.password = [password]
spring.secondDatasource.driverClassName = oracle.jdbc.OracleDriver
Then, somewhere in #Configuration class:
#Bean(name = "db1")
#ConfigurationProperties(prefix="spring.db1")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().build();
}

Configure Multiple DataSource in Spring Boot with JNDI

I want to manage multiple DataSource using your Application Servers built-in features and access it using JNDI. I am using Spring boot with Spring JPA data.
I am able to configure the application.properties for single datasource:
spring.datasource.jndi-name=jdbc/customers
And my configuration in context.xml file as below:
<Resource name="jdbc/customer" auth="Container" type="javax.sql.DataSource"
maxTotal="100" maxIdle="30" maxWaitMillis="10000"
username="root" password="root" driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://localhost:3306/customer"/>
Everything works fine.
But when I am unable to configure for two datasource.
I am sure on the configuration in context.xml file:
<Resource name="jdbc/customer" auth="Container" type="javax.sql.DataSource"
maxTotal="100" maxIdle="30" maxWaitMillis="10000"
username="root" password="root" driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://localhost:3306/customer"/>
<Resource name="jdbc/employee" auth="Container" type="javax.sql.DataSource"
maxTotal="100" maxIdle="30" maxWaitMillis="10000"
username="root" password="root" driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://localhost:3306/employee"/>
I am in doubt about the application.properties file configuration.
I tried the below options with no success:
spring.datasource.jndi-name=jdbc/customers,jdbc/employee
Please let me know any details on Spring boot with JNDI for multiple data source. I was looking for this configuration for days now.
Second Trial As per Spring Boot Documentation
spring.datasource.primary.jndi-name=jdbc/customer
spring.datasource.secondary.jndi-name=jdbc/project
Configuration class.
#Bean
#Primary
#ConfigurationProperties(prefix="datasource.primary")
public DataSource primaryDataSource() {
return DataSourceBuilder.create().build();
}
#Bean
#ConfigurationProperties(prefix="datasource.secondary")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().build();
}
The application does not get started. Though the tomcat server is getting started. No errors are printed in the log.
Third Trial: With JndiObjectFactoryBean
I have the below application.properties
spring.datasource.primary.expected-type=javax.sql.DataSource
spring.datasource.primary.jndi-name=jdbc/customer
spring.datasource.primary.jpa.database-platform=org.hibernate.dialect.MySQL5Dialect
spring.datasource.primary.jpa.show-sql=false
spring.datasource.primary.jpa.hibernate.ddl-auto=validate
spring.datasource.secondary.jndi-name=jdbc/employee
spring.datasource.secondary.expected-type=javax.sql.DataSource
spring.datasource.secondary.jpa.database-platform=org.hibernate.dialect.MySQL5Dialect
spring.datasource.secondary.jpa.show-sql=false
spring.datasource.secondary.jpa.hibernate.ddl-auto=validate
And the below java configuration:
#Bean(destroyMethod="")
#Primary
#ConfigurationProperties(prefix="spring.datasource.primary")
public FactoryBean primaryDataSource() {
return new JndiObjectFactoryBean();
}
#Bean(destroyMethod="")
#ConfigurationProperties(prefix="spring.datasource.secondary")
public FactoryBean secondaryDataSource() {
return new JndiObjectFactoryBean();
}
But still getting error:
Related cause: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'primaryDataSource' defined in class path resource [com/web/initializer/MvcConfig.class]: Invocation of init method failed; nested exception is javax.naming.NameNotFoundException: Name [jdbc/customer] is not bound in this Context. Unable to find [jdbc].
Related cause: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'secondaryDataSource' defined in class path resource [com/web/initializer/MvcConfig.class]: Invocation of init method failed; nested exception is javax.naming.NameNotFoundException: Name [jdbc/employee] is not bound in this Context. Unable to find [jdbc].
at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.onRefresh(EmbeddedWebApplicationContext.java:133)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:474)
at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.refresh(EmbeddedWebApplicationContext.java:118)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:686)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:320)
at org.springframework.boot.context.web.SpringBootServletInitializer.run(SpringBootServletInitializer.java:117)
at org.springframework.boot.context.web.SpringBootServletInitializer.createRootApplicationContext(SpringBootServletInitializer.java:108)
at org.springframework.boot.context.web.SpringBootServletInitializer.onStartup(SpringBootServletInitializer.java:68)
at org.springframework.web.SpringServletContainerInitializer.onStartup(SpringServletContainerInitializer.java:175)
Update:
Trial using the below properties file:
spring.datasource.primary.expected-type=javax.sql.DataSource
spring.datasource.primary.jndi-name=java:comp/env/jdbc/customer
spring.datasource.secondary.jndi-name=java:comp/env/jdbc/employee
spring.datasource.secondary.expected-type=javax.sql.DataSource
spring.jpa.database-platform=org.hibernate.dialect.MySQL5Dialect
spring.jpa.show-sql=false
spring.jpa.hibernate.ddl-auto=validate
It creates all the tables in customer schema, but fails trying to find the other tables also.(from the second schema)
This is the solution for your third trial a little bit modified.
Consider this solution (Spring Boot 1.3.2):
application.properties file:
spring.datasource.primary.jndi-name=java:/comp/env/jdbc/SecurityDS
spring.datasource.primary.driver-class-name=org.postgresql.Driver
spring.datasource.secondary.jndi-name=java:/comp/env/jdbc/TmsDS
spring.datasource.secondary.driver-class-name=org.postgresql.Driver
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQL9Dialect
spring.jpa.show-sql=false
Configuration:
#Configuration# EnableConfigurationProperties
public class AppConfig {
#Bean# ConfigurationProperties(prefix = "spring.datasource.primary")
public JndiPropertyHolder primary() {
return new JndiPropertyHolder();
}
#Bean# Primary
public DataSource primaryDataSource() {
JndiDataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
DataSource dataSource = dataSourceLookup.getDataSource(primary().getJndiName());
return dataSource;
}
#Bean# ConfigurationProperties(prefix = "spring.datasource.secondary")
public JndiPropertyHolder secondary() {
return new JndiPropertyHolder();
}
#Bean
public DataSource secondaryDataSource() {
JndiDataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
DataSource dataSource = dataSourceLookup.getDataSource(secondary().getJndiName());
return dataSource;
}
private static class JndiPropertyHolder {
private String jndiName;
public String getJndiName() {
return jndiName;
}
public void setJndiName(String jndiName) {
this.jndiName = jndiName;
}
}
}
And then you can follow guide http://docs.spring.io/spring-data/jpa/docs/1.3.0.RELEASE/reference/html/jpa.repositories.html to use your datasources with jpa repositories.
You could use a plain JndiObjectFactoryBean for this. Simply replace the DataSourceBuilder with a JndiObjectFactoryBean should do the trick.
Java configuration
#Bean(destroyMethod="")
#Primary
#ConfigurationProperties(prefix="datasource.primary")
public FactoryBean primaryDataSource() {
return new JndiObjectFactoryBean();
}
#Bean(destroyMethod="")
#ConfigurationProperties(prefix="datasource.secondary")
public FactoryBean secondaryDataSource() {
return new JndiObjectFactoryBean();
}
Properties
datasource.primary.jndi-name=jdbc/customer
datasource.primary.expected-type=javax.sql.DataSource
datasource.secondary.jndi-name=jdbc/project
datasource.secondary.expected-type=javax.sql.DataSource
You can set every property of the JndiObjectFactoryBean using the #ConfigurationProperties annotation. (See the expected-type I added, but you could also set cache or lookup-on-startup etc.).
Note: when doing a JNDI lookup set the destroyMethod to an "" else you might get the situation that when the application is shutdown your JNDI resource is getting closed/shutdown as well. This is not something you want in a shared environment.
It works for me and contains less code
#Configuration
public class Config {
#Value("${spring.datasource.primary.jndi-name}")
private String primaryJndiName;
#Value("${spring.datasource.secondary.jndi-name}")
private String secondaryJndiName;
private JndiDataSourceLookup lookup = new JndiDataSourceLookup();
#Primary
#Bean(destroyMethod = "") // destroy method is disabled for Weblogic update app ability
public DataSource primaryDs() {
return lookup.getDataSource(primaryJndiName);
}
#Bean(destroyMethod = "") // destroy method is disabled for Weblogic update app ability
public DataSource secondaryDs() {
return lookup.getDataSource(secondaryJndiName);
}
}
In my case, when i start my application using Spring Boot App, the database configurations are read on application-dev.properties, when I publish on tomcat, using datasources, was necessary add a validation to check if my profile is prod, in this case, i do a jndi lookup
#Bean(name = "dsName")
#ConfigurationProperties("ds.datasource.configuration")
public DataSource dataSource(#Qualifier("dsProperties") DataSourceProperties db1DataSourceProperties)
{
if(Arrays.asList(environment.getActiveProfiles()).contains("prod"))
{
final JndiDataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
return dataSourceLookup.getDataSource("java:comp/env/jdbc/DS1");
}
else
{
return db1DataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
}
}
The concise way I get success and explore more
set up many jndi resources in external tomcat , that you can start/stop in eclipse.Note- double click the tomcat in eclipse and select use workspace metedata , means dont deploy the app to tomcat webapp folder. Add jndi resources in respective eclipse server files( context.xml - ResourceLink, server.xml - Resource , web.xml - resource-ref).
no need to set spring.datasource.* in application.properties. since jndi-contest which is a datasource type( i.e. type="javax.sql.DataSource") is exported to external server.
in SpringBootApplication annotated class , create the datasource beans from all the jndi resources(those setup as per #1) through jndi lookup
#Bean(name = "abcDataSource")
public DataSource getAbcDataSource() {
JndiDataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
DataSource dataSource = dataSourceLookup.getDataSource("java:comp/env/jdbc/abcDataSource");
return dataSource;
}
if spring jdbc used in your project then provide the above datasource to create a jdbcTemplate bean
#Bean(name = "jdbcAbcTemplate")
public JdbcTemplate abcJdbcTemplate(#Lazy #Qualifier("abcDataSource")
DataSource refDS)
{
return new JdbcTemplate(refDS);
}
just autowire a property of DataSource type and get systemout its details to explore more.
While the above answers are good I am going to add one more to illustrate a deadly gotcha if mixing jndi and full data connection configuration. In a typical development environment you may fully qualify the database connection in your local dev environment then use jndi as you push to qa, etc.. Your
application .properties looks like so:
spring.datasource.url=jdbc:sqlserver://server.domain.org:1433;databaseName=dbxx
spring.datasource.username=userxxyyzz
spring.datasource.password=passxxyyzz
spring.datasource.platform=mssql
spring.datasource.driverClassName=com.microsoft.sqlserver.jdbc.SQLServerDriver
and application-qa.properties like so:
spring.datasource.jndi-name=java:jboss/datasources/dbxx
The problem arises when you have to define your own beans to have multiple datasources. If you use the default Spring managed datasource then it automatically detects jndi vs fully qualified connection and returns a datasource with no change needed in the application code. If you define your own datasource it no longer does this. If you have application.properties like so:
spring.custom.datasource.url=jdbc:sqlserver://server.domain.org:1433;databaseName=dbxx
spring.custom.datasource.username=userxxyyzz
spring.custom.datasource.password=passxxyyzz
spring.custom.datasource.platform=mssql
spring.custom.datasource.driverClassName=com.microsoft.sqlserver.jdbc.SQLServerDriver
and application-qa.properties like so:
spring.custom.datasource.jndi-name=java:jboss/datasources/dbxx
with a datasource bean like so, as suggested in Spring docs https://docs.spring.io/spring-boot/docs/2.1.11.RELEASE/reference/html/howto-data-access.html
#Primary
#Bean(name="customDataSourcePropertiesBean")
#ConfigurationProperties("spring.custom.datasource")
public DataSourceProperties customDataSourceProperties() {
return new DataSourceProperties();
}
#Primary
#Bean(name="customDataSourceBean")
#ConfigurationProperties("spring.custom.datasource")
public HiakriDataSource customDataSource(#Qualifier("customDataSourcePropertiesBean") DataSourceProperties properties) {
return properties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
}
This datasource builder does not attempt to read the jndi config in application-qa.properties and silently fails back to application.properties returning the WRONG database connection. Resolution is fairly simple - test which environment you are in and customize the type of database connection created. Debugging this was a bear, as the symptom was that the app appeared to be ignoring application-qa.properties. I share to spare others the pain. Add spring.profiles.active=qa etc. to your properties files to know which environment you are in then:
#Value("${spring.profiles.active}")
String profile;
#Value("${spring.custom.jndi-name}")
String jndi;
#Primary
#Bean(name="customDataSourcePropertiesBean")
#ConfigurationProperties("spring.custom.datasource")
public DataSourceProperties customDataSourceProperties() {
return new DataSourceProperties();
}
#Primary
#Bean(name="customDataSourceBean")
#ConfigurationProperties("spring.custom.datasource")
public DataSource customDataSource(#Qualifier("customDataSourcePropertiesBean") DataSourceProperties properties) {
if(profile.equals("localhost")) {
return DataSourceBuilder
.create()
.username(properties.getDataUsername())
.password(properties.getPassword())
.url(properties.getUrl())
.driverClassName(properties.getDriverClassName())
.build();
}else {
JndiDataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
return dataSourceLookup.getDataSource(jndi);
}
}

Categories

Resources