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.


About Intentwise:

Intentwise is a Chicago-based advertising technology company that helps brands, sellers and agencies amplify their Amazon advertising effectiveness. Intentwise’s industry-leading SAAS platform harnesses the power of machine learning and AI to provide impactful recommendations, diagnostics, and automation.

1 reply

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

You have to agree to the comment policy.

This site uses Akismet to reduce spam. Learn how your comment data is processed.

JOIN NOW!
JOIN NOW!
SUBSCRIBE TO OUR NEWSLETTER
Never miss a story from Intentwise.
Join Now!