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 datasources 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 which 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 recommended for a production environment. It’s best to create the tables yourself in a production environment.

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

Configure 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 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 application you will have multiple datasources 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 datasources.

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 datasources 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 datasource is loaded first at the boot time. If the datasource 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 datasources, remember to configure the entity scan package in both the datasources, or it would mislead you and take away lot of your time in debugging the issue, especially when the working code suddenly starts failing.