Article

Improve your Envers revision info table

Improve your Hibernate Envers revision info table with custom attributes, better date and increased performance.

Envers Hibernate Spring Boot
Intermediate
Florian Beaufumé
Florian Beaufumé
Published 31 Aug 2023 - 4 min read
Improve your Envers revision info table

Table of contents

Introduction

Hibernate Envers (Envers for short) is a powerful and popular framework for Java applications to audit and version your entities. It tracks changes in entities (creations, updates and deletions) and store them in audit tables (one for each entity class). In addition, Envers stores extra data in a revision info table such as the date of the business operation and the revision identifier.

In this article, I will share several tips to customize the content of the Envers revision info table and improve its performances.

Default revision info table

When a business entity is created, modified or deleted, Envers stores a copy of that entity in a dedicated audit table. For example when a User entity is modified by the application code, Hibernate will, as expected, execute an SQL UPDATE on the USER table:

ID FIRST_NAME LAST_NAME
123 John Doe

Envers will also execute an SQL INSERT on the USER_AUD table:

ID REV REVTYPE FIRST_NAME LAST_NAME
123 1 1 John Doe

An audit table contains a copy of the original business attributes and two extra columns (and possibly other columns, but this is beyond the scope of this article):

  • The REVTYPE column describes the type of change: 0 for a creation, 1 for a modification and 2 for a deletion.
  • The REV column contains the revision number. It starts at 1 and is a global identifier for all changes of the application. All entity changes performed during the same application transaction use the same revision number.

The revision number is also stored in a dedicated table, named REVINFO by default, along with the timestamp of the transaction:

REV REVTSTMP
1 1688915781586

The revision info table is nice but could be better. We may want to add extra fields such as: the user who performed the business operation, the URI of the HTTP request that executed the business operation. We also may want to use a more human readable date format.

Also, let's have a look at the SQL queries executed by Envers to write the audit data to a PostgreSQL database:

select nextval('hibernate_sequence')
insert into revinfo (rev, revtstmp) values (?, ?)
insert into user_aud (id, rev, revtype, first_name, last_name) values (?, ?, ?, ?, ?)

In addition to the two expected SQL INSERT statements, we notice an extra SQL request for a sequence access. This call may depend on the database used, but comes from the Envers choice of primary key handling for the revision entity. We may want to use a different primary key strategy to prevent this extra database call. Note that when properly used sequences can bring performance benefits but there are tradeoffs. This is beyond the scope of this article.

Customized revision info table

In this section we will improve the revision info table:

  • Add the name of the user who performed the business operation
  • Add the HTTP URI of the request that executed the business operation
  • Use a more human readable date format
  • Remove the sequence call
  • Rename the table to REVISION_INFO

The revision info table is filled by Envers through the revision info entity. To improve the revision info table, we will customize the revision info entity. The first approach to do so it to extend the base revision info entity provided by Envers, i.e. org.hibernate.envers.DefaultRevisionEntity. But this does not work well with some of our objectives (using a more human readable date format, and removing the sequence call). Instead, we will directly provide our own revision info entity inspired by DefaultRevisionEntity:

import org.hibernate.envers.RevisionEntity;
import org.hibernate.envers.RevisionNumber;
import org.hibernate.envers.RevisionTimestamp;

import javax.persistence.*;
import java.io.Serializable;

@Entity
@Table(name = "revision_info")
@RevisionEntity(CustomRevisionListener.class)
public class CustomRevisionEntity implements Serializable {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@RevisionNumber
private long rev;

@RevisionTimestamp
private Date date;

private String username;

private String requestPath;

// Getters, setters, equals and hashCode omitted
}

Standard JPA annotations are used to customize the entity: @Table to choose a different table name and @GeneratedValue to use a primary key generation strategy that does not need a sequence. The @RevisionTimestamp Envers annotation is placed on a Date attribute rather than a long to get a human readable date. Custom attributes are defined as regular JPA entity attributes, but need a special listener configured by the @RevisionEntity Envers annotation.

import org.hibernate.envers.RevisionListener;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.security.Principal;
import java.util.Optional;

public class CustomRevisionListener implements RevisionListener {

@Override
public void newRevision(Object object) {
CustomRevisionEntity entity = (CustomRevisionEntity) object;

// Retrieve the current username from Spring Security
entity.setUsername(Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
.map(Principal::getName)
.orElse(null));

// Retrieve the current request path from Spring Web
entity.setRequestPath(Optional.ofNullable(RequestContextHolder.getRequestAttributes())
.filter(ServletRequestAttributes.class::isInstance)
.map(ServletRequestAttributes.class::cast)
.map(ServletRequestAttributes::getRequest)
.map(this::getRequestPath)
.orElse(null));
}

/**
* Return a nice request path such as "GET /foo/bar?acme=12".
*/

private String getRequestPath(HttpServletRequest request) {
StringBuilder builder = new StringBuilder(512);
builder.append(request.getMethod()).append(' ').append(request.getRequestURI());

String queryString = request.getQueryString();
if (queryString != null) {
builder.append('?').append(queryString);
}

return builder.toString();
}
}

The listener must implement the right Envers interface and allows us to update the attributes of the revision info entity instances. In the previous example we extract the current username from Spring Security and build a request path using information from Spring Web. Other attributes may be added in the custom revision info entity and defined in the listener.

The REVISION_INFO table is now nicer:

REV DATE USERNAME REQUEST_PATH
1 2023-07-17 21:32:54.717 johndoe PUT /users/123

And the SQL requests executed by Envers to write the audit data are now without sequence call:

insert into revision_info (date, username, request_path) values (?, ?, ?)
insert into user_aud (id, rev, revtype, first_name, last_name) values (?, ?, ?, ?, ?)

Conclusion

In this article we saw how the revision info table of Envers can be customized using a couple classes. Feel free to further customize the table through JPA annotations or extra attributes.

© 2007-2024 Florian Beaufumé