Transaction Management across two data sources (ChainedTransactionManager) - SpringBoot - spring

Why Spring ChainedTransactionManager is deprecated? Do spring provides any alternative lib to support multiple transaction managers?
My use-case:- We are building a spring boot application that is connected to two data sources let's say (db1 and db2), which performs insert operation on both databases (db1 and db2). And our requirement is something like this :
insert -> DB1 -> SUCCESSFUL
insert -> DB2 -> ERROR
ROLLBACK DB1
Currently, we are using ChaninedTransactionManager and it is working as expected, but I Can see that lib is deprecated? So, just wanted to make sure is it safe to use that, or does Spring provide any alternate lib which we can use as a replacement for this?

ChainedTransactionManager
The configured instances will start transactions in the order given and commit/rollback in reverse order, which means the PlatformTransactionManager most likely to break the transaction should be the last in the list configured.
If you chained transactions in this order : transaction1, transaction2
transaction1 begin
transaction2 begin
transaction2 commit -> error rollbacks, rollbacks transction1 too
transaction1 commit -> error, only rollbacks transaction1
The case insert -> DB1 -> SUCCESSFUL insert -> DB2 -> ERROR ROLLBACK DB1 is working.
But, if you had insert -> DB1 -> FAIL ROLLBAK DB1 -> DB2 -> SUCCESSFUL, the insert is commited for DB2 and not for DB1. More details in this article.
If your are OK with this, you can copy the class in your project et keep using it : https://github.com/spring-projects/spring-data-commons/issues/2232#issuecomment-1018473289
JtaTransactionManager with Atomikos
To have all transactions rollback if one fails, I changed my conf to use JtaTransactionManager (with spring boot and posgtres).
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
application.properties
my.datasource.one.unique-resource-name=
my.datasource.one.xa-properties.url=jdbc:postgresql://
my.datasource.one.xa-data-source-class-name=org.postgresql.xa.PGXADataSource
my.datasource.one.xa-properties.user=
my.datasource.one.xa-properties.password=
my.datasource.one.max-pool-size=
my.datasource.one.min-pool-size=
my.datasource.two.unique-resource-name=
my.datasource.two.xa-properties.url=jdbc:postgresql://
my.datasource.two.xa-data-source-class-name=org.postgresql.xa.PGXADataSource
my.datasource.two.xa-properties.user=
my.datasource.two.xa-properties.password=
my.datasource.two.max-pool-size=
my.datasource.two.min-pool-size=
#Bean
#Primary
#ConfigurationProperties("my.datasource.one")
public DataSource dataSourceOne() {
return new AtomikosDataSourceBean();
}
#Bean("entityManagerOne")
#DependsOn("transactionManager")
public LocalContainerEntityManagerFactoryBean entityManagerOne(#Autowired JpaVendorAdapter jpaVendorAdapter) {
HashMap<String, Object> properties = new HashMap<>();
properties.put("hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect");
properties.put("hibernate.default_schema", "public");
properties.put("hibernate.ddl-auto", "none");
LocalContainerEntityManagerFactoryBean entityManager = new LocalContainerEntityManagerFactoryBean();
entityManager.setJtaDataSource(dataSourceOne());
entityManager.setJpaVendorAdapter(jpaVendorAdapter);
entityManager.setPackagesToScan("");
entityManager.setPersistenceUnitName("");
entityManager.setJpaPropertyMap(properties);
return entityManager;
}
#Bean
#Primary
#ConfigurationProperties("my.datasource.two")
public DataSource dataSourceTwo() {
return new AtomikosDataSourceBean();
}
#Bean("entityManagerTwo")
#DependsOn("transactionManager")
public LocalContainerEntityManagerFactoryBean entityManagerTwo(#Autowired JpaVendorAdapter jpaVendorAdapter) {
HashMap<String, Object> properties = new HashMap<>();
properties.put("hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect");
properties.put("hibernate.default_schema", "public");
properties.put("hibernate.ddl-auto", "none");
LocalContainerEntityManagerFactoryBean entityManager = new LocalContainerEntityManagerFactoryBean();
entityManager.setJtaDataSource(dataSourceTwo());
entityManager.setJpaVendorAdapter(jpaVendorAdapter);
entityManager.setPackagesToScan("");
entityManager.setPersistenceUnitName("");
entityManager.setJpaPropertyMap(properties);
return entityManager;
}
jta.properties
com.atomikos.icatch.enable_logging=false
com.atomikos.icatch.default_jta_timeout=60000000
com.atomikos.icatch.max_timeout=100000000
com.atomikos.icatch.threaded_2pc=true
#Bean
public JpaVendorAdapter jpaVendorAdapter(#Autowired Environment env) {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
hibernateJpaVendorAdapter.setShowSql(false);
return hibernateJpaVendorAdapter;
}
#Bean
public UserTransaction userTransaction() throws SystemException {
var userTransaction = new UserTransactionImp();
userTransaction.setTransactionTimeout(60000);
return userTransaction;
}
#Bean
public TransactionManager atomikosTransactionManager() throws SystemException {
var userTransactionManager = new UserTransactionManager();
userTransactionManager.setForceShutdown(true);
return userTransactionManager;
}
#Bean
public PlatformTransactionManager transactionManager(#Autowired UserTransaction userTransaction, #Autowired TransactionManager atomikosTransactionManager) throws Throwable {
return new JtaTransactionManager(userTransaction, atomikosTransactionManager);
}
Enable prepared transactions for postgres
postgresql.conf
max_prepared_transactions = 100 # zero disables the feature
Sources that helped me to get this :
https://www.baeldung.com/java-atomikos
http://www.thedevpiece.com/configuring-multiple-datasources-using-springboot-and-atomikos/
https://github.com/YihuaWanglv/spring-boot-jta-atomikos-sample

Because there is an API improvement. From the docs
https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/transaction/ChainedTransactionManager.html
Instead of using ChainedTransactionManager for attaching callbacks to transaction commit (pre commit/post commit), either register a TransactionSynchronization to explicitly follow transaction cleanup with simplified semantics in case of exceptions.
You are free to still use it, just keep in mind that at some point in the time that class will be removed in future versions of Spring and upgrading will not be possible without refactoring of that part.

Related

How can i connect to 2 different Schemas on one datasource in springboot

I am trying to extract data from multiple databases in different servers. This has been successful after configuring different Data sources as shown in my code however i cannot see to configure 2 different Data sources on the same connection.
I have tried to create 2 beans within the config file but i would still need to set the Data source.
#Configuration
#EnableJpaRepositories(basePackages = {"balances.Repository.World"},
entityManagerFactoryRef = "ASourceEntityManager",transactionManagerRef = "ASourceTransactionManager")
#EnableTransactionManagement
public class World{
#Autowired
private Environment env;
#Bean
public LocalContainerEntityManagerFactoryBean ASourceEntityManager() {
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setDataSource(ASourceDatasource());
em.setPackagesToScan(new String("balances.Repository.World"));
em.setPersistenceUnitName("ASourceEntityManager");
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
em.setJpaVendorAdapter(vendorAdapter);
HashMap<String, Object> properties = new HashMap<String, Object>();
properties.put("hibernate.dialect",env.getProperty("app.datasource.ASource.hibernate.dialect"));
properties.put("hibernate.show-sql",env.getProperty("app.datasource.ASource.show-sql"));
em.setJpaPropertyMap(properties);
return em;
}
#Bean
#ConfigurationProperties(prefix = "app.datasource.ASource")
public DataSource ASourceDatasource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName(env.getProperty("app.datasource.ASource.driverClassName"));
dataSource.setUrl(env.getProperty("app.datasource.ASource.url"));
dataSource.setUsername(env.getProperty("app.datasource.ASource.username"));
dataSource.setPassword(env.getProperty("app.datasource.ASource.password"));
return dataSource;
}
#ConfigurationProperties(prefix = "app.datasource.ASourceB")
public DataSource ASourceDatasource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName(env.getProperty("app.datasource.ASourceB.driverClassName"));
dataSource.setUrl(env.getProperty("app.datasource.ASourceB.url"));
dataSource.setUsername(env.getProperty("app.datasource.ASourceB.username"));
dataSource.setPassword(env.getProperty("app.datasource.ASourceB.password"));
return dataSource;
}
#Bean
public PlatformTransactionManager ASourceTransactionManager() {
JpaTransactionManager transactionManager
= new JpaTransactionManager();
transactionManager.setEntityManagerFactory(
ASourceEntityManager().getObject());
return transactionManager;
}
After some thought i have come up with a workaround that might be a bit hacky but gets the job done.
Instead of declaring a new database connection for the second schema in my app.properties i have used one connection. I placed all my entities in one model package and used a native query to access the other schema. In the native query i would then specify the schema eg:
select * from DATABASE1.Id;
This solution does not scale well as it would create a lot of work when dealing with a large number of entities so if there is a way of specifying the schema in the repository that would also help.
I tried using the entity attributes to define my schema but jpa seems to be ignoring it and prefixing the table with the wrong schema for example if
i annotate my class with the following
#Table(name = "Payment", schema = "DATABASE1", catalog = "")
The resulting query would be "select * from DATABASE2.Payment" instead of
select * from DATABASE1.Payment

Spring: Allow missing database connection

I have a project connecting to multiple databases (Oracle & SQLServer), some of which are not always mandatory nor available when starting the application - especially on a dev environment.
I'm looking for a way to allow the application to start without a database connection right-on. We only require a connection to these DB when doing specific tasks, which use a Staging Database requiring a specific network access not always available on dev.
Here, with our Oracle connection (which is the one being under a specific network) :
We had a configuration that allowed the application to start, but was throwing errors when running the Tests because of the erroneous HikariDataSource cast :
#Configuration
#EnableJpaRepositories(
basePackages = "com.bar.foo.repository.oracle",
entityManagerFactoryRef = "oracleEntityManager",
transactionManagerRef = "oracleTransactionManager"
)
public class OracleConfiguration {
private final Logger log = LoggerFactory.getLogger(this.getClass());
#Inject
private Environment environment;
#Value("${spring.datasource.oracle.ddl-auto:none}")
private String hibernateddlAuto;
#Value("${spring.datasource.oracle.dialect:org.hibernate.dialect.Oracle10gDialect}")
private String hibernateDialect;
#Bean
#Primary
#ConfigurationProperties("spring.datasource.oracle")
public DataSourceProperties oracleDataSourceProperties() {
return new DataSourceProperties();
}
#Bean
#Primary
#ConfigurationProperties("spring.datasource.hikari")
public DataSource oracleDataSource() {
HikariDataSource ds = (HikariDataSource) oracleDataSourceProperties().initializeDataSourceBuilder().build();
ds.setPoolName(environment.getProperty("spring.datasource.oracle.poolName"));
return ds;
}
#Bean
#Primary
public LocalContainerEntityManagerFactoryBean oracleEntityManager() {
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setPackagesToScan("com.bar.foo.domain.oracle");
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
em.setJpaVendorAdapter(vendorAdapter);
HashMap<String, Object> properties = new HashMap<>();
properties.put("hibernate.ddl-auto", hibernateddlAuto);
properties.put("hibernate.dialect", hibernateDialect);
em.setJpaPropertyMap(properties);
em.setDataSource(oracleDataSource());
return em;
}
#Primary
#Bean
public PlatformTransactionManager oracleTransactionManager() {
JpaTransactionManager transactionManager
= new JpaTransactionManager();
transactionManager.setEntityManagerFactory(
oracleEntityManager().getObject());
return transactionManager;
}
}
I moved this configuration to extend HikariConfig as following :
#Configuration
#EnableJpaRepositories(
basePackages = "com.bar.foo.repository.oracle",
entityManagerFactoryRef = "oracleEntityManager",
transactionManagerRef = "oracleTransactionManager"
)
#ConfigurationProperties("spring.datasource.oracle")
public class OracleConfiguration extends HikariConfig{
#Bean
#Primary
public DataSource oracleDataSource() {
return new HikariDataSource(this);
}
#Bean
#Primary
public LocalContainerEntityManagerFactoryBean oracleEntityManager() {
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setPackagesToScan("com.bar.foo.domain.oracle");
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
em.setJpaVendorAdapter(vendorAdapter);
HashMap<String, Object> properties = new HashMap<>();
properties.put("hibernate.ddl-auto", hibernateddlAuto);
properties.put("hibernate.dialect", hibernateDialect);
em.setJpaPropertyMap(properties);
em.setDataSource(oracleDataSource());
return em;
}
#Bean
#Primary
public PlatformTransactionManager oracleTransactionManager() {
JpaTransactionManager transactionManager
= new JpaTransactionManager();
transactionManager.setEntityManagerFactory(
oracleEntityManager().getObject());
return transactionManager;
}
Which does not start if the Oracle DB is unavailable.
Using an embedded DB would not suit our use case, as we do not always need this connection, and when we do we need specific data replicated from Production.
Splitting this on multiple microservice/applications is also a no-go, as it would be a huge refacto, and does not really fit our use case (we aggregate data from multiple sources into a final one).
Is there a simple way to allow this ?
HikariCP provides some really nice configuration properties which may suit your needs. Specifically (the first one on that list) initializationFailTimeout:
This property controls whether the pool will "fail fast" if the pool
cannot be seeded with an initial connection successfully...
A value
less than zero will bypass any initial connection attempt, and the
pool will start immediately while trying to obtain connections in the
background. Consequently, later efforts to obtain a connection may
fail.
If you want to solve your problem this way, i.e. by hiding any initialization failures (by setting a negative initializationFailTimeout value), then you'll just have to make sure you have the right logic in case the DB is unaccesible/down when your getting a connection from the pool.
Well, after looking more at the HikariConfig class, I spotted the
initializationFailTimeout which states
* #param initializationFailTimeout the number of milliseconds before the
* pool initialization fails, or 0 to validate connection setup but continue with
* pool start, or less than zero to skip all initialization checks and start the
* pool without delay.
Setting it to zero or below allows the application to start, however it takes way much longer than it used to, and setting it below zero does not prevent it from waiting two connections timeouts.
I ended up also setting the connectionTimeout to the minimum 250ms on our dev configuration file, it seems to be working fine for now.
An alternative solution is to set the following parameter:
spring.jpa.database-platform=org.hibernate.dialect.Oracle10gDialect
Using that setting, I did not experience any slowness in application startup as I did when using the initializationFailTimeout setting.

Spring Data JPA and Connection Pooling

I have a RESTful web application which stores data against Oracle and the stack is Spring-Data-Jpa and Hibernate.
We have implemented connection pooling using Oracle UCP, but it does not seem to work. There were 1000s of connections in the DB for our NFT tests.
My config is as shown below
#Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() throws IllegalStateException, SQLException {
LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
entityManagerFactoryBean.setDataSource(dataSource());
entityManagerFactoryBean.setPersistenceProviderClass(HibernatePersistenceProvider.class);
entityManagerFactoryBean.setPackagesToScan(env.getRequiredProperty(ENTITYMANAGER_PACKAGES_TO_SCAN));
entityManagerFactoryBean.setJpaProperties(hibProperties());
return entityManagerFactoryBean;
}
#Bean
public DataSource dataSource() throws IllegalStateException, SQLException {
PoolDataSource dataSource = PoolDataSourceFactory.getPoolDataSource();
dataSource.setConnectionFactoryClassName(env.getRequiredProperty(DB_CONNECTION_FACTORY_CLASS_NAME));
dataSource.setURL(env.getRequiredProperty(DATABASE_URL));
dataSource.setUser(env.getRequiredProperty(DATABASE_USERNAME));
dataSource.setPassword(env.getRequiredProperty(DATABASE_PASSWORD));
dataSource.setMinPoolSize(Integer.parseInt(env.getRequiredProperty(DATABASE_CONNECTION_MIN_POOL_SIZE)));
dataSource.setMaxPoolSize(Integer.parseInt(env.getRequiredProperty(DATABASE_CONNECTION_MAX_POOL_SIZE)));
dataSource.setInitialPoolSize(Integer.parseInt(env.getRequiredProperty(DATABASE_CONNECTION_INITIAL_POOL_SIZE)));
return dataSource;
}
private Properties hibProperties() {
Properties properties = new Properties();
properties.put(HIBERNATE_DIALECT, env.getRequiredProperty(HIBERNATE_DIALECT));
properties.put(HIBERNATE_SHOW_SQL, env.getRequiredProperty(HIBERNATE_SHOW_SQL));
/*properties.put(HIBERNATE_NAMING_STRATEGY, env.getRequiredProperty(HIBERNATE_NAMING_STRATEGY));*/
return properties;
}
Let me know if there is another config I should be using here, such that a new session is not created all the time.
Thanks
Kris
It would seem that a DataSource gets created for each thread and centralising the datasource in the Jetty config sorted the problem.
jetty and Oracle Connection Pooling

How to define HSQ DB properties in Spring JPA using annoations

I am running HSQL DB as a Im memory using run manger Swing.I have to connect the HSQLDB server from Spring JPA repository using annotations.
My repository class.
#RepositoryRestResource
public interface Vehicle extends JpaRepository<Vehicle , BigInteger>{
public List<Vehicle > findAll(Sort sort);
}
service Method:
#Service
public class LocationService {
#Autowired
VehicletRepository vehicleRepository = null;
/**
* This method is to get all the locations from the repository
*/
public List<Vehicle> getVehicless() {
Order order = new Order(Direction.ASC,"vehicleCode");
Sort sort = new Sort(order);
List<Airport> airports = vehicletRepository .findAll(sort);
System.out.println("inside service");
return vehicles;
}
}
Anyone help to achieve Spring JPA conenction with HSQL DB using annotations.
I assume you dont use Spring boot:
you need #Configuration class -(it basically is new way to configure spring applications in java ) with #EnableJpaRepositories which turn it spring data jpa/ spring data rest for you. You will also have to specify your entity manager, transaction manager and data source beans. Example below:
#Configuration
#EnableJpaRepositories("your.package.with.repositories")
public class DBConfig{
#Bean
public JpaTransactionManager transactionManager() {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(entityManagerFactory().getObject());
return transactionManager;
}
#Bean
public DataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
DBConfigurationCommon.configureDB(dataSource, your_jdbc_url_here, db_username_here, db_password_here);
return dataSource;
}
#Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
entityManagerFactoryBean.setDataSource(dataSource());
entityManagerFactoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
//you may need to define new Properties(); here with hibernate dialect and add it to entity manager factory
entityManagerFactoryBean.setPackagesToScan("your_package_with_domain_classes_here");
return entityManagerFactoryBean;
}
}

AbstractRoutingDatasource and hibernate.dialect with multiples datasources

In my Spring Java Config file,i use AbstractRoutingDatasource successfully to switch multiple databases connections.
public MyRoutingDataSource myRoutingDataSource() {
MyRoutingDataSource dataSource = new MyRoutingDataSource();
Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
dataSource.setDefaultTargetDataSource(defaultDataSource());
dataSource.setTargetDataSources(targetDataSources);
return dataSource;
}
But when i want specific SQL requests that differs from datasources PostgreSQL/Oracle etc.. (like pagination limit/rowNums), i have to associated a specific hibernate dialect.
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
vendorAdapter.setGenerateDdl(false);
vendorAdapter.setShowSql(true);
vendorAdapter.setDatabasePlatform("org.hibernate.dialect.PostgreSQL81Dialect");
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(vendorAdapter);
factory.setPackagesToScan("fr.appli.model");
factory.setDataSource(myRoutingDataSource());
My question is how can i change hibernate dialect in my EntityManager, when i change datasource from with AbstractRoutingDatasource ? Is it possible to do it programmatically ?
Thanks
I think to achive this you will need multiple datasource configuration files and each database has its own SessionFactory object in an DAO which will permof DB operations.
For example :
SessionFactory sessionFactory1 = new Configuration().configure("oracleconfig.cfg.xml").buildSessionFactory();
SessionFactory sessionFactory2 = new Configuration().configure("mysqlconfig.cfg.xml").buildSessionFactory();

Resources