November 25th, 2018

               

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.


About Intentwise:

Intentwise is a Chicago-based technology company that helps brands, sellers, and agencies maximize returns from Amazon advertising spend. Intentwise’s industry-leading SaaS platform provides impactful recommendations and automation to accelerate advertising optimization while saving valuable time for advertisers.

3 replies
  1. Raghavendra Seshadri
    Raghavendra Seshadri says:

    Hi Dilip,

    Yes that is possible and that is what we have done, If you look at the blog post , the class UserRevEntity uses a custom sequence and this sequence value will be stored as revision_id in the audit table.

    Hope this answers your question

    Thanks

    Reply
  2. Dilip
    Dilip says:

    I have condition that I have a difference sequence name than hibernate_sequence, I am getting cannot insert NULL into primary column in the audit table, is there a way to make hibernate use user created sequence for audit tables?

    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!