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.
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):
REVTYPE
column describes the type of change: 0 for a creation, 1 for a modification and 2 for a deletion.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.
In this section we will improve the revision info table:
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 (?, ?, ?, ?, ?)
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é