Hibernate Envers and Spring Boot with Multiple Data Sources

Share

The Hibernate Envers project aims to enable easy auditing of Persistent classes.  It completely takes away the hassles of auditing an entity.

The section below outlines the high-level steps to configure Envers with Spring boot using Custom Revision Entity. It demonstrates how Envers can be configured when multiple data sources are involved.

Getting Started with Envers

If you are using Maven, add the below configuration for Envers in pom.xml

org.hibernate 
hibernate-envers

Creating Custom Revision Entity

In many scenarios, you would need a custom revision entity as default revision entity fields may not suffice. Below is an example of creating a Custom Revision entity.

The example uses a sequence for id, but there can be different strategies that can be used for id generation.

Pay attention to @RevisionNumber and @RevisionEntity which are used by Envers for creating Revision Entity and persisting the value in the database.

package com.example.service.audit.entity @Table(name = "app_user_rev_entity", schema = "application") @Entity @RevisionEntity(UserRevisionListener.class) public class UserRevEntity implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user_rev_generator") @SequenceGenerator(name = "user_rev_generator", allocationSize = 10,sequenceName = "app_userrev_seq") @RevisionNumber private int id; @RevisionTimestamp @Temporal(TemporalType.TIMESTAMP) private Date date; @Column(name = "user_name") private String userName; @Column(name = "user_id") private Long userId; // Getters, setters, equals, hashcode ….

UserRevisionListener is the class where all the custom attributes for the UserRevEntity are populated.

The example below used Spring Boot principal user to get the username. Similarly, other attributes can also be populated. The class should implement the RevisionListener interface from Hibernate Envers.

package com.example.service.audit.entity public class UserRevisionListener implements RevisionListener { /** * @see org.hibernate.envers.RevisionListener#newRevision(java.lang.Object) */ @Override public void newRevision(Object userRevision) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); User authenticatedUser = (User) authentication.getPrincipal(); UserRevEntity userRevEntity = (UserRevEntity) userRevision; userRevEntity.setUserName(authenticatedUser.getUsername()); userRevEntity.setDate(Calendar.getInstance(TimeZone.getTimeZone("UTC")).getTime()); }

Configure application.properties

Below are some of the configurations specific to Hibernate Envers. More details about the configuration properties can be found here.

spring.jpa.properties.org.hibernate.envers.revision_type_field_name=revision_type spring.jpa.properties.org.hibernate.envers.revision_field_name=revision_id spring.jpa.properties.org.hibernate.envers.modified_flag_suffix=_mod spring.jpa.properties.org.hibernate.envers.audit_strategy=org.hibernate.envers.strategy.ValidityAuditStrategy

You can use spring.jpa.hibernate.ddl-auto=update if you want Hibernate to create custom revision entity and other audit tables for you, but this is not something I would recommend for a production environment. It’s best to create the tables yourself in a production environment.

The next section is about the audit strategy. Default is good but ValidityAuditStrategy is a more advanced strategy.

Configure an Entity to be Audited

Below is an example of how to audit an Entity class. All we need to do is add @Audited annotation at the class level (if all the attributes have to be audited) or add the annotation as an individual attribute level.

@Entity @Table(name = "app_user", schema = "application") public class UserEntity implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user_generator") @SequenceGenerator(name = "user_generator", allocationSize = 1, sequenceName = "application.app_userid_seq") @Column(name = "user_id") private Long userId; @NotNull @Column(name = "first_name") @Audited private String firstName;

Configure Envers with Multiple Datasources

In many applications, you will have multiple data sources as you may want to store different data in different schemas or maybe even different databases. The section below outlines the steps involved in configuring Envers with multiple data sources.

Configure multiple datasources in application.properties file.

application.spring.datasource.url = jdbc:postgresql://xxxx application.spring.datasource.username = xxx application.spring.datasource.password=xxx application.spring.datasource.testWhileIdle = true application.spring.datasource.validationQuery = SELECT 1 application.spring.datasource.schema=application application.spring.datasource.driver-class-name=org.postgresql.Driver example.spring.datasource.url = jdbc:postgresql://xxx example.spring.datasource.username = xxx example.spring.datasource.password=xxx example.spring.datasource.testWhileIdle = true example.spring.datasource.validationQuery = SELECT 1 example.spring.datasource.schema=public example.spring.datasource.driver-class-name=org.postgresql.Driver

Below are the configurations needed for Spring Boot to identify and load the data source to be used.

@Configuration @EnableTransactionManagement @EnableJpaRepositories(entityManagerFactoryRef = "exampleEntityManagerFactory", transactionManagerRef = "exampleTransactionManager", basePackages = { "com.example.service.example.entity" }) public class ExampleDatabaseConfig { @Bean(name = "exampleDataSource") @ConfigurationProperties(prefix = "example.spring.datasource") public DataSource dataSource() { return DataSourceBuilder.create().build(); } @Bean(name = "exampleEntityManagerFactory") public LocalContainerEntityManagerFactoryBean exampleEntityManagerFactory(EntityManagerFactoryBuilder builder, @Qualifier("exampleDataSource") DataSource dataSource) { return builder.dataSource(dataSource).packages("com.example.service.audit.entity").persistenceUnit("example").build(); } @Bean(name = "exampleTransactionManager") public PlatformTransactionManager exampleTransactionManager( @Qualifier("exampleEntityManagerFactory") EntityManagerFactory exampleEntityManagerFactory) { return new JpaTransactionManager(exampleEntityManagerFactory); } } @Configuration @EnableTransactionManagement @EnableJpaRepositories(entityManagerFactoryRef = "applicationEntityManagerFactory", transactionManagerRef = "applicationTransactionManager", basePackages = { "com.application.service.example.entity " }) public class ApplicationDatabaseConfig { @Primary @Bean(name = "applicationDataSource") @ConfigurationProperties(prefix = "application.spring.datasource") public DataSource dataSource() { return DataSourceBuilder.create().build(); } @Primary @Bean(name = "applicationEntityManagerFactory") public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder, @Qualifier("applicationDataSource") DataSource dataSource) { return builder.dataSource(dataSource) .packages("com.example.service.audit.entity") .persistenceUnit("application").build(); } @Primary @Bean(name = "applicationTransactionManager") public PlatformTransactionManager transactionManager( @Qualifier("applicationEntityManagerFactory") EntityManagerFactory entityManagerFactory) { return new JpaTransactionManager(entityManagerFactory); } }

In both the above configurations, the important piece to remember is to add the package where Envers Custom Revision Entity class is implemented — in this case add “com.example.service.audit.entity” to both the Datasource configuration files for Entity scan by Envers.

If you miss adding the package in one of the configuration files, you might suddenly start seeing the below errors when trying to persist the entity which is being audited. The code might not have changed, but you may start seeing failures.

This depends on the ClassLoader on which the data source is loaded first at the boot time. If the data source which has the entity scan package missing is loaded last, you will see the below error.

This may lead to a lot of time being wasted in debugging the issue and you may be misled into creating the missing hibernate_sequence, a generic sequence used by Hibernate Envers.

Caused by: org.hibernate.exception.SQLGrammarException: could not extract ResultSet at org.hibernate.exception.internal.SQLStateConversionDelegate.convert(SQLStateConversionDelegate.java:106) at org.hibernate.exception.internal.StandardSQLExceptionConverter.convert(StandardSQLExceptionConverter.java:42) at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:109) at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:95) at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.extract(ResultSetReturnImpl.java:79) at org.hibernate.id.enhanced.SequenceStructure$1.getNextValue(SequenceStructure.java:96) at org.hibernate.id.enhanced.NoopOptimizer.generate(NoopOptimizer.java:40) at org.hibernate.id.enhanced.SequenceStyleGenerator.generate(SequenceStyleGenerator.java:412) at org.hibernate.event.internal.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:101) at org.hibernate.jpa.event.internal.core.JpaSaveEventListener.saveWithGeneratedId(JpaSaveEventListener.java:56) at org.hibernate.event.internal.DefaultSaveOrUpdateEventListener.saveWithGeneratedOrRequestedId(DefaultSaveOrUpdateEventListener.java:192) at org.hibernate.event.internal.DefaultSaveEventListener.saveWithGeneratedOrRequestedId(DefaultSaveEventListener.java:38) at org.hibernate.event.internal.DefaultSaveOrUpdateEventListener.entityIsTransient(DefaultSaveOrUpdateEventListener.java:177) at org.hibernate.event.internal.DefaultSaveEventListener.performSaveOrUpdate(DefaultSaveEventListener.java:32) at org.hibernate.event.internal.DefaultSaveOrUpdateEventListener.onSaveOrUpdate(DefaultSaveOrUpdateEventListener.java:73) at org.hibernate.internal.SessionImpl.fireSave(SessionImpl.java:679) at org.hibernate.internal.SessionImpl.save(SessionImpl.java:671) at org.hibernate.envers.internal.revisioninfo.DefaultRevisionInfoGenerator.saveRevisionData(DefaultRevisionInfoGenerator.java:75) at org.hibernate.envers.internal.synchronization.AuditProcess.getCurrentRevisionData(AuditProcess.java:119) at org.hibernate.envers.internal.synchronization.AuditProcess.executeInSession(AuditProcess.java:96) ... 141 common frames omitted Caused by: org.postgresql.util.PSQLException: ERROR: relation "hibernate_sequence" does not exist Position: 17

Conclusion

Hibernate Envers is a mature auditing module provided by Hibernate. It is highly configurable and saves the effort of building an auditing framework.

However, when using multiple data sources, remember to configure the entity scan package in both the data sources, or it would mislead you and take away a lot of your time in debugging the issue, especially when the working code suddenly starts failing.

Read our post “Front-end Browser Debugging” for tools and tricks to troubleshoot issues with your browser.

Recommendations

Subscribe to our Newsletter​

Learn about product updates, webinars, and news for ecommerce professionals.