Spring Boot-Adding EntityListeners to application with multiple data sources
It’s one of the edge cases where your Spring Boot application is connected to multiple databases and you want to perform Entity listener operations while performing read/write operations on the database.
To configure multiple datasources. You need to have seperate DAOConfig for both the databases
package com.medium.app.config;import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;import org.apache.commons.dbcp.BasicDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder.Builder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
entityManagerFactoryRef = "db1EntityManagerFactory",
transactionManagerRef ="db1TransactionManager",
basePackages={"com.medium.app.repository.db1"}
)
public class Db1DAOConfig
{
@Bean(name = "db1DataSourceProperties")
@ConfigurationProperties("db1.datasource")
public DataSourceProperties db1DataSourceProperties() {
return new DataSourceProperties();
}@Bean(name = "db1DataSource")
public DataSource db1DataSource()
{
return db1DataSourceProperties().
initializeDataSourceBuilder().
type(BasicDataSource.class).build();
}
@Bean(name = "db1EntityManagerFactory")
public LocalContainerEntityManagerFactoryBean
db1EntityManagerFactory(final EntityManagerFactoryBuilder builder)
{Builder dataSource = builder.dataSource(db1DataSource());
return dataSource.
packages("com.medium.app.entity.db1").persistenceUnit("db1").build();}
@Bean(name = "db1TransactionManager")
public PlatformTransactionManager db1TransactionManager(@Qualifier("db1EntityManagerFactory")EntityManagerFactory db1EntityManagerFactory)
{
return new JpaTransactionManager(db1EntityManagerFactory);
}
}
Now let’s configure second database-
package com.medium.app.config;import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;import com.zaxxer.hikari.HikariDataSource;@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
entityManagerFactoryRef = "db2EntityManagerFactory",
transactionManagerRef="db2TransactionManager",
basePackages="com.medium.app.repository.db2"
)
public class Db2DAOConfig
{
@Bean(name = "db2DataSourceProperties")
@Primary
@ConfigurationProperties("db2.datasource")
public DataSourceProperties db2DataSourceProperties() {
return new DataSourceProperties();
}
@Primary
@Bean(name = "db2DataSource")
public DataSource db2DataSource()
{
return db2DataSourceProperties().
initializeDataSourceBuilder().
type(HikariDataSource.class).build();
}
@Primary
@Bean(name = "db2EntityManagerFactory")
public LocalContainerEntityManagerFactoryBean
entityManagerFactory(
EntityManagerFactoryBuilder builder,
@Qualifier("db2DataSource")DataSource dataSource){
return builder.dataSource(dataSource)
.packages("com.medium.app.entity.db2").
persistenceUnit("db2").build();
}
@Primary
@Bean(name="db2TransactionManager")
public PlatformTransactionManager db2TransactionManager(@Qualifier("db2EntityManagerFactory")EntityManagerFactory db2EntityManagerFactory)
{
return new JpaTransactionManager(db2EntityManagerFactory);
}
}
Here’s the configuration of application.properties-
db2.datasource.driver = com.mysql.cj.jdbc.Driver
db2.datasource.url= jdbc:mysql://localhost:3306/db2
db2.datasource.username=root
db2.datasource.password=rootspring.jpa.properties.hibernate.enable_lazy_load_no_trans=truedb1.datasource.driver=com.mysql.cj.jdbc.Driver
db1.datasource.url=jdbc:mysql://localhost:3306/db1
db1.datasource.username=root
db1.datasource.password=root
Now, let’s say we want to perform audit while save/update operations on a table. For this spring-data provides facility to attach EntityListeners to your entity
@Entity
@Table(name = "commission_details")
@EntityListeners(value=CommissionDetailsEntityListener.class)public class CommissionDetails
{
@Id
@Column(name = "id")
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id; @Column(name = "domain_id")
private Long domainId; @Column(name = "status")
private Long status;
}
The problem with enititylisteners is that they aren’t managed by Spring and are loaded during application start-up so any efforts to inject an @Autowired bean here fails.
So, idea is to get hold of entityManagerFactory from the config and use it manage the persistence.
Here’s the sample code for the same-
package com.medium.app.entity.db2.listeners;import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.PrePersist;
import javax.persistence.PreRemove;
import javax.persistence.PreUpdate;
import javax.transaction.Transactional;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import static javax.transaction.Transactional.TxType.MANDATORY;
import com.medium.app.entity.db2.CommissionDetails;
import com.medium.app.entity.db2.CommissionDetailsAudit;public class CommissionDetailsEntityListener
{@PrePersist
public void prePersist(CommissionDetails commissionDetails)
{
perform(commissionDetails, "CREATE");
}
@PreUpdate
public void preUpdate(CommissionDetails commissionDetails)
{
perform(commissionDetails, "UPDATE");
}
@PreRemove
public void preRemove(CommissionDetails commissionDetails)
{
perform(commissionDetails, "DELETED");
}
@Transactional(MANDATORY)
private void perform(CommissionDetails commissionDetails, String action)
{
CommissionDetailsAudit commissionDetailsAudit = new CommissionDetailsAudit();
commissionDetailsAudit.setAction(action); commissionDetailsAudit.setDomainId(commissionDetails.getDomainId());
commissionDetailsAudit.setStatus(commissionDetails.getStatus());
Object entityManager = BeanUtil.getBean("db2EntityManagerFactory");
System.out.println(entityManager);
if(entityManager instanceof EntityManagerFactory)
{
EntityManagerFactory nativeEntityManagerFactory = (EntityManagerFactory) entityManager;
EntityManager entityManagerObj = nativeEntityManagerFactory.createEntityManager();
entityManagerObj.getTransaction().begin();
entityManagerObj.persist(commissionDetailsAudit);
entityManagerObj.getTransaction().commit();
entityManagerObj.close();
}
}
}
Here BeanUtil is-
@Service
public class BeanUtil implements ApplicationContextAware
{
private static ApplicationContext context;
public static <T> T getBean(Class<T> beanClass)
{
return context.getBean(beanClass);
}public static Object getBean(String beanName)
{
return context.getBean(beanName);
}public void setApplicationContext(ApplicationContext applicationContext) throws BeansException
{
context = applicationContext;
}}