Home | Send Feedback

Entity auditing with Hibernate Envers

Published: 5. August 2019  •  java

A common requirement in a business application is to store versioning information when particular data changes; when something changed, who changed it and what changed. In this blog post, we are going to take a look at Hibernate Envers, a component of the Hibernate JPA library, that provides an easy auditing/versioning solution for entity classes. Envers works with Hibernate and JPA, and you can use Envers anywhere Hibernate works.

Envers stores changes in special audit tables and provides several query methods to access historical snapshots.

Setup

To start with Envers, you first need to add the library to your project's classpath. In a Maven managed file, you add this dependency.

      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-envers</artifactId>
      <version>6.4.1.Final</version>
    </dependency>
    <dependency>

pom.xml

Next, annotate the entity classes or entity properties, that Envers should keep track of, with the @Audited annotation. For the following examples, I use two entity classes: Employee and Company. In the Employee class, I added the @Audited to the class, Envers keeps track of all properties in this class.

@Entity
@Audited(withModifiedFlag = true)
public class Employee {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private int id;

  private String lastName;

  private String firstName;

  private String street;

  private String city;

  @ManyToOne
  private Company company;

Employee.java

In the Company class, I added @Audited only to the name property. Envers keeps track of this property and ignores all the others. Envers also provides a @NotAudited annotation for cases where you add @Audited to the class but want to ignore specific properties.

@Entity
public class Company {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private int id;

  @Audited
  private String name;

  private String street;

  private String city;

  @OneToMany(mappedBy = "company", cascade = CascadeType.ALL, orphanRemoval = true)
  private Set<Employee> employees;

Company.java

Make sure your entities using immutable unique identifiers (primary keys).

Tables

In the following example project, I set the Hibernate configuration hibernate.hbm2ddl.auto to update.

      <property name="hibernate.hbm2ddl.auto" value="update" />

persistence.xml

With this configuration in place, Hibernate automatically creates the two tables Employee and Company. For each @Audited entity, it creates an audit table where Envers keeps track of the changes. It also creates the REVINFO table, which keeps track of the revision number and the timestamp when the change happened.

er

Each time an @Audited entity is going to be inserted, updated or deleted, Envers steps in and creates a new revision by inserting a new row into the REVINFO table and the corresponding AUD table.


Notice that the EMPLOYEE_AUD table contains a field for each field in the EMPLOYEE table, whereas the COMPANY_AUD table only contains the name field, that's because we only annotated the name property in the Company class. The ID field of the AUD tables is the primary key of the corresponding entity table; that's the reason your primary keys have to be immutable.


In the Employee class, we enabled the withModifiedFlag option (@Audited(withModifiedFlag = true)) which is disabled by default. You see the effect of this option in the EMPLOYEE_AUD table. Envers added for each property an additional _MOD boolean property. This modification flag stores information if a property has been changed at the given revision.

In the Company class, we didn't enable this option. Therefore the COMPANY_AUD table does not contain a NAME_MOD field.

For comparison here the EMPLOYEE_AUD table definition if you don't enable the option:
without withModifiedFlag

You should only enable withModifiedFlag if you need this information because the trade-off is that the additional fields increase the size of the audit tables. There is one Envers query (forRevisionsOfEntityWithChange) that depends on this additional information, so if you plan to use this query method, you have to enable the option.

Custom Revision Entity

Notice that Envers, by default, only keeps track of the date and time when a change occurred. However, in a multi-user application, you often also want to know who did the change.

For this purpose, we need to create a custom revision entity class. This class has to extend the DefaultRevisionEntity class and has to be annotated with @Entity and @RevisionEntity. You can add any property you like, and they will be stored in the REVINFO table together with the revision number and the timestamp.

import jakarta.persistence.Entity;
import jakarta.persistence.Table;

import org.hibernate.envers.DefaultRevisionEntity;
import org.hibernate.envers.RevisionEntity;

@Entity
@Table(name = "REVINFO")
@RevisionEntity(CustomRevisionEntityListener.class)
public class CustomRevisionEntity extends DefaultRevisionEntity {

  private static final long serialVersionUID = 1L;

  private String username;

  public String getUsername() {
    return this.username;
  }

  public void setUsername(String username) {
    this.username = username;
  }
}

CustomRevisionEntity.java

The listener we specify with @RevisionEntity must implement the RevisionListener interface. There is only one method we have to implement: newRevision. In this method, we need to fill in our additional properties, in this case, the username. We don't have to touch the revision number and timestamp, Envers sets them automatically.

import org.hibernate.envers.RevisionListener;

public class CustomRevisionEntityListener implements RevisionListener {

  @Override
  public void newRevision(Object revisionEntity) {
    CustomRevisionEntity customRevisionEntity = (CustomRevisionEntity) revisionEntity;
    customRevisionEntity.setUsername(CurrentUser.INSTANCE.get());
  }
}

CustomRevisionEntityListener.java

For this demo application, we store the logged in user into a ThreadLocal variable. The listener above extracts it with CurrentUser.INSTANCE.get() and we set the username with CurrentUser.INSTANCE.set(....)

public class CurrentUser {

  public static final CurrentUser INSTANCE = new CurrentUser();

  private static final ThreadLocal<String> storage = new ThreadLocal<>();

  public void logIn(String user) {
    storage.set(user);
  }

  public void logOut() {
    storage.remove();
  }

  public String get() {
    return storage.get();
  }
}

CurrentUser.java

With this configuration in place, the REVINFO table now contains a new field: username

custom revision properties

I copied these three classes straight from the Envers documentation. Visit this URL for more information: https://docs.jboss.org/hibernate/orm/5.4/userguide/html_single/Hibernate_User_Guide.html#envers-revisionlog

Examples

In this section, we are going insert, update, and delete some data and see how Envers stores the changes.

Revision 1: Insert

First, the user "Alice" inserts a company and two employees.

    EntityManager em = JPAUtil.getEntityManagerFactory().createEntityManager();
    CurrentUser.INSTANCE.logIn("Alice");

    em.getTransaction().begin();
    Company company = new Company();
    company.setName("E Corp");
    company.setCity("New York City");
    company.setStreet(null);

    Set<Employee> employees = new HashSet<>();

    Employee employee = new Employee();
    employee.setCompany(company);
    employee.setLastName("Spencer");
    employee.setFirstName("Linda");
    employee.setStreet("High Street 123");
    employee.setCity("Newark");
    employees.add(employee);

    employee = new Employee();
    employee.setCompany(company);
    employee.setLastName("Ralbern");
    employee.setFirstName("Michael");
    employee.setStreet("57th Street");
    employee.setCity("New York City");
    employees.add(employee);

    company.setEmployees(employees);

    em.persist(company);
    em.getTransaction().commit();

Main.java

Notice that you don't have to call any special Envers methods. Just write standard JPA (or Hibernate) code. Behind the scenes, Envers listens for any update and automatically inserts the audit information into the database.

Envers inserted a new row into the REVINFO table with the timestamp and the username. Because we inserted the three entities in one transaction, only one REVINFO row was created.

Envers also inserted a new row into COMPANY_AUD for the new Company, REVTYPE of 0 denotes an INSERT operation. Moreover, Envers inserted two new rows into the EMPLOYEE_AUD table. Because in an insert, all properties changed, all _MOD fields contain the value true.

REVINFO
+----+---------------+----------+
| ID |   TIMESTAMP   | USERNAME |
+----+---------------+----------+
|  1 | 1564997410711 | Alice    |
+----+---------------+----------+

COMPANY_AUD
+----+-----+---------+--------+
| ID | REV | REVTYPE |  NAME  |
+----+-----+---------+--------+
|  1 |   1 |       0 | E Corp |
+----+-----+---------+--------+

EMPLOYEE_AUD
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+
| ID | REV | REVTYPE |      CITY      | CITY_MOD | FIRSTNAME | FIRSTNAME_MOD | LASTNAME | LASTNAME_MOD |      STREET      | STREET_MOD | COMPANY_ID | COMPANY_MOD |
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+
|  1 |   1 |       0 | New York City  |     TRUE | Michael   |          TRUE | Ralbern  |         TRUE | 57th Street      |       TRUE |          1 |        TRUE |
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+
|  2 |   1 |       0 | Newark         |     TRUE | Linda     |          TRUE | Spencer  |         TRUE | High Street 123  |       TRUE |          1 |        TRUE |
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+

Revision 2: Update Company

In the next transaction, "Bob" changes the name of the company from "E Corp" to "EEE Corp".

    CurrentUser.INSTANCE.logIn("Bob");

    em.getTransaction().begin();
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<Company> q = cb.createQuery(Company.class);
    Root<Company> c = q.from(Company.class);
    ParameterExpression<String> p = cb.parameter(String.class);
    q.select(c).where(cb.equal(c.get("name"), p));

    TypedQuery<Company> query1 = em.createQuery(q);
    query1.setParameter(p, "E Corp");

    company = query1.getSingleResult();
    company.setName("EEE Corp");

    em.getTransaction().commit();

Main.java

Envers creates a new revision and inserts a new row into the COMPANY_AUD table. REVTYPE = 1 denotes an UPDATE operation.

REVINFO
+----+---------------+----------+
| ID |   TIMESTAMP   | USERNAME |
+----+---------------+----------+
|  1 | 1564997410711 | Alice    |
+----+---------------+----------+
|  2 | 1564997410849 | Bob      |
+----+---------------+----------+

COMPANY_AUD
+----+-----+---------+----------+
| ID | REV | REVTYPE |   NAME   |
+----+-----+---------+----------+
|  1 |   1 |       0 | E Corp   |
+----+-----+---------+----------+
|  1 |   2 |       1 | EEE Corp |
+----+-----+---------+----------+

Revision 3: New Employee

"Bob" inserts a new employee: Janet Robinson

    CurrentUser.INSTANCE.logIn("Bob");

    em.getTransaction().begin();
    employee = new Employee();
    employee.setCompany(company);
    employee.setLastName("Robinson");
    employee.setFirstName("Janet");
    employee.setCity("Greenwich");
    employee.setStreet("Walsh Ln 10");
    company.getEmployees().add(employee);
    em.getTransaction().commit();

Main.java

REVINFO
+----+---------------+----------+
| ID |   TIMESTAMP   | USERNAME |
+----+---------------+----------+
|  1 | 1564997410711 | Alice    |
+----+---------------+----------+
|  2 | 1564997410849 | Bob      |
+----+---------------+----------+
|  3 | 1564997410858 | Bob      |
+----+---------------+----------+


EMPLOYEE_AUD
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+
| ID | REV | REVTYPE |      CITY      | CITY_MOD | FIRSTNAME | FIRSTNAME_MOD | LASTNAME | LASTNAME_MOD |      STREET      | STREET_MOD | COMPANY_ID | COMPANY_MOD |
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+
|  1 |   1 |       0 | New York City  |     TRUE | Michael   |          TRUE | Ralbern  |         TRUE | 57th Street      |       TRUE |          1 |        TRUE |
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+
|  2 |   1 |       0 | Newark         |     TRUE | Linda     |          TRUE | Spencer  |         TRUE | High Street 123  |       TRUE |          1 |        TRUE |
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+
|  3 |   3 |       0 | Greenwich      |     TRUE | Janet     |          TRUE | Robinson |         TRUE | Walsh Ln 10      |       TRUE |          1 |        TRUE |
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+

Revision 4: Update Employee

"Alice" updates the street and city of the employee Linda Spencer

    CurrentUser.INSTANCE.logIn("Alice");

    em.getTransaction().begin();
    TypedQuery<Employee> query2 = createEmployeeQuery(em, "Linda", "Spencer");
    employee = query2.getSingleResult();
    employee.setStreet("101 W 91st St");
    employee.setCity("New York City");
    em.getTransaction().commit();

Main.java

REVINFO
+----+---------------+----------+
| ID |   TIMESTAMP   | USERNAME |
+----+---------------+----------+
|  1 | 1564997410711 | Alice    |
+----+---------------+----------+
|  2 | 1564997410849 | Bob      |
+----+---------------+----------+
|  3 | 1564997410858 | Bob      |
+----+---------------+----------+
|  4 | 1564997410873 | Alice    |
+----+---------------+----------+

Here we see that only CITY_MOD and STREET_MOD are set to true because these are the only two properties we changed in our code.

EMPLOYEE_AUD
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+
| ID | REV | REVTYPE |      CITY      | CITY_MOD | FIRSTNAME | FIRSTNAME_MOD | LASTNAME | LASTNAME_MOD |      STREET      | STREET_MOD | COMPANY_ID | COMPANY_MOD |
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+
|  1 |   1 |       0 | New York City  |     TRUE | Michael   |          TRUE | Ralbern  |         TRUE | 57th Street      |       TRUE |          1 |        TRUE |
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+
|  2 |   1 |       0 | Newark         |     TRUE | Linda     |          TRUE | Spencer  |         TRUE | High Street 123  |       TRUE |          1 |        TRUE |
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+
|  3 |   3 |       0 | Greenwich      |     TRUE | Janet     |          TRUE | Robinson |         TRUE | Walsh Ln 10      |       TRUE |          1 |        TRUE |
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+
|  2 |   4 |       1 | New York City  |     TRUE | Linda     |         FALSE | Spencer  |        FALSE | 101 W 91st St    |       TRUE |          1 |       FALSE |
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+

Revision 5: Delete Employee

Alice deletes the employee Michael Ralbern

    CurrentUser.INSTANCE.logIn("Alice");

    em.getTransaction().begin();
    TypedQuery<Employee> query3 = createEmployeeQuery(em, "Michael", "Ralbern");
    employee = query3.getSingleResult();
    employee.getCompany().getEmployees().remove(employee);
    em.remove(employee);
    em.getTransaction().commit();

Main.java

REVINFO
+----+---------------+----------+
| ID |   TIMESTAMP   | USERNAME |
+----+---------------+----------+
|  1 | 1564997410711 | Alice    |
+----+---------------+----------+
|  2 | 1564997410849 | Bob      |
+----+---------------+----------+
|  3 | 1564997410858 | Bob      |
+----+---------------+----------+
|  4 | 1564997410873 | Alice    |
+----+---------------+----------+
|  5 | 1564997410892 | Alice    |
+----+---------------+----------+

Here we see the REVTYPE 2, which denotes a DELETE operation. For delete operations, all properties are set to NULL.

EMPLOYEE_AUD
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+
| ID | REV | REVTYPE |      CITY      | CITY_MOD | FIRSTNAME | FIRSTNAME_MOD | LASTNAME | LASTNAME_MOD |      STREET      | STREET_MOD | COMPANY_ID | COMPANY_MOD |
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+
|  1 |   1 |       0 | New York City  |     TRUE | Michael   |          TRUE | Ralbern  |         TRUE | 57th Street      |       TRUE |          1 |        TRUE |
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+
|  2 |   1 |       0 | Newark         |     TRUE | Linda     |          TRUE | Spencer  |         TRUE | High Street 123  |       TRUE |          1 |        TRUE |
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+
|  3 |   3 |       0 | Greenwich      |     TRUE | Janet     |          TRUE | Robinson |         TRUE | Walsh Ln 10      |       TRUE |          1 |        TRUE |
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+
|  2 |   4 |       1 | New York City  |     TRUE | Linda     |         FALSE | Spencer  |        FALSE | 101 W 91st St    |       TRUE |          1 |       FALSE |
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+
|  1 |   5 |       2 | NULL           |     TRUE | NULL      |          TRUE | NULL     |         TRUE | NULL             |       TRUE |       NULL |        TRUE |
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+

Queries

Storing audit/version information is one side of the story, but we also need a way to access this information when we need it. For this purpose, Envers provides several methods to query the audit tables.

The main entry point to the query methods is the class AuditReader. You create an instance with AuditReaderFactory.get and pass the EntityManager instance as the argument.

    EntityManager em = JPAUtil.getEntityManagerFactory().createEntityManager();
    AuditReader reader = AuditReaderFactory.get(em);

MainQuery.java

The getRevision() method returns a list of revision numbers, at which an entity was modified. The method expects the entity class and the primary key of the entity as parameters.

    List<Number> revisions = reader.getRevisions(Company.class, 1);
    for (Number rev : revisions) {
      System.out.println(rev);

MainQuery.java

With getRevisionDate() we have access to the revision date (REVINFO.TIMESTAMP).

      Date revisionDate = reader.getRevisionDate(rev);
      System.out.println(revisionDate);

MainQuery.java

To access our custom username field in the revision table, we need to call findRevision() and pass the class of our custom revision entity class and the revision number as arguments.

      CustomRevisionEntity revision = reader.findRevision(CustomRevisionEntity.class,
          rev);
      String username = revision.getUsername();
      System.out.println(username);

MainQuery.java

With find(), we get an entity by primary key and the given revision.

      Company comp = reader.find(Company.class, 1, rev);
      String name = comp.getName();
      String street = comp.getStreet();
      System.out.println(name);
      System.out.println(street);

MainQuery.java

The applications prints the following output.

1
Mon Aug 05 07:46:25 CEST 2019
Alice
E Corp
null
------------------------------------------------
2
Mon Aug 05 07:46:25 CEST 2019
Bob
EEE Corp
null

The company was changed in revision 1 (insert) and in revision 2 (update name). Notice that the street property of the company instance we get back from find() is null because we only audit the name property.

You can also call find() with a revision number where the given entity class was not changed. In revision 5, we deleted an employee. The find() returns the state of the Company at that given revision.

    Company comp = reader.find(Company.class, 1, 5);
    String name = comp.getName();
    System.out.println(name); // output: EEE Corp

MainQuery.java

The library also provides a find() variant that takes a Date object instead of a revision number as the third argument. This will then return the entity in the state as it was at that particular date.


Another useful method is getRevisionNumberForDate(). This method returns the highest revision number, which was created on or before the given date.

    Calendar cal = Calendar.getInstance();
    Number revNumber = reader.getRevisionNumberForDate(cal.getTime());
    System.out.println(revNumber); // output: 5

MainQuery.java


AuditQuery

Let's look at more advanced queries, all members of the AuditQuery class which you access with AuditReader.createQuery().

The forEntitiesAtRevision() query returns all entities of a given class at a specific revision. In the first example, we want all Employee objects in revision 1. We get back the two instances that we inserted in revision 1.

    AuditQuery query = reader.createQuery().forEntitiesAtRevision(Employee.class, 1);
    query.add(AuditEntity.relatedId("company").eq(1));
    for (Employee e : (List<Employee>) query.getResultList()) {
      System.out.println(e.getId() + ": " + e.getLastName() + " " + e.getFirstName());
    }
    // 1: Ralbern Michael
    // 2: Spencer Linda

MainQuery.java

All queries that return an AuditQuery instance can be restricted further with the add() method. In the example above, we only get employees that are related to the company that has a primary key of 1.

If we run the query with revision 2, we get back the same two employee instances even we didn't change anything in revision 2 related to employees. The class returns, like the find() method, the state of the entity at that given revision, it does not matter if the entity changed in this revision or not.

    query = reader.createQuery().forEntitiesAtRevision(Employee.class, 2);

MainQuery.java

    // 1: Ralbern Michael
    // 2: Spencer Linda

When we query revision 5, we see that the output changes. Because we inserted a new employee in revision 3 and deleted an employee in revision 5.

    query.add(AuditEntity.relatedId("company").eq(1));

MainQuery.java

    // 3: Robinson Janet
    // 2: Spencer Linda

To demonstrate another where clause, here an example where we only want entities with the last name equals "Spencer".

    query.add(AuditEntity.property("lastName").eq("Spencer"));
    // query.add(AuditEntity.or(AuditEntity.property("lastName").eq("Spencer"),

MainQuery.java

    // 2: Spencer Linda

You can also combine multiple where clauses with AuditEntity.or() and AuditEntity.and()

query.add(AuditEntity.or(AuditEntity.property("lastName").eq("Spencer"), AuditEntity.property("lastName").eq("Robinson")));

forEntitiesAtRevision() by default does not return deleted entities. You can change this by passing true as the third argument.

    query = reader.createQuery().forEntitiesAtRevision(Employee.class,
        Employee.class.getName(), 5, true);
    for (Employee e : (List<Employee>) query.getResultList()) {
      System.out.println(e.getId() + ": " + e.getLastName() + " " + e.getFirstName());
    }
    // 3: Robinson Janet
    // 2: Spencer Linda
    // 1: null null

MainQuery.java

Notice that all properties, except the primary key of a deleted entity, are null.


The next method is forEntitiesModifiedAtRevision() which only returns entities that are affected in the given revision. Like with all AuditQuery you can further restrict the result with query.add()

When we query revision 1, we get back two employees because we inserted them in this revision.

    query = reader.createQuery().forEntitiesModifiedAtRevision(Employee.class, 1);
    for (Employee e : (List<Employee>) query.getResultList()) {
      System.out.println(e.getId() + ": " + e.getLastName() + " " + e.getFirstName());
    }
    // 1: Ralbern Michael
    // 2: Spencer Linda

MainQuery.java

When we query revision 2, we get back an empty list, because, in revision 2, we changed the company and didn't change any employee.

    query = reader.createQuery().forEntitiesModifiedAtRevision(Employee.class, 2);
    for (Employee e : (List<Employee>) query.getResultList()) {
      System.out.println(e.getId() + ": " + e.getLastName() + " " + e.getFirstName());
    }
    // empty

MainQuery.java

In revision 5, we deleted an employee, so we get back only this deleted entity.

    for (Employee e : (List<Employee>) query.getResultList()) {
      System.out.println(e.getId() + ": " + e.getLastName() + " " + e.getFirstName());
    }
    // 1: null null

MainQuery.java


forRevisionsOfEntity() returns a list of revisions, at which the given entity class was modified. The result is a list of three-element arrays containing the entity class (0), the revision entity (1) and the type of revision (2)

If you set the second boolean argument to true, the method returns a list of entity classes instead of a list with three-element arrays.

The third boolean argument specifies whether the query should return deleted entities (true) or not (false).

    // query.add(AuditEntity.id().eq(1));
    List<Object[]> results = query.getResultList();
    for (Object[] result : results) {
      Employee employee = (Employee) result[0];
      CustomRevisionEntity revEntity = (CustomRevisionEntity) result[1];
      RevisionType revType = (RevisionType) result[2];

      System.out.println("Revision     : " + revEntity.getId());
      System.out.println("Revision Date: " + revEntity.getRevisionDate());
      System.out.println("User         : " + revEntity.getUsername());
      System.out.println("Type         : " + revType);
      System.out.println(
          "Employee     : " + employee.getLastName() + " " + employee.getFirstName());

      System.out.println("------------------------------------------------");
    }

MainQuery.java

Output of the code above. Notice that revision 2 is not listed because we only changed a Company in that particular revision.

Revision : 1
Revision Date: Mon Aug 05 07:46:25 CEST 2019
User : Alice
Type : ADD
Employee : Ralbern Michael
------------------------------------------------
Revision : 1
Revision Date: Mon Aug 05 07:46:25 CEST 2019
User : Alice
Type : ADD
Employee : Spencer Linda
------------------------------------------------
Revision : 3
Revision Date: Mon Aug 05 07:46:25 CEST 2019
User : Bob
Type : ADD
Employee : Robinson Janet
------------------------------------------------
Revision : 4
Revision Date: Mon Aug 05 07:46:25 CEST 2019
User : Alice
Type : MOD
Employee : Spencer Linda
------------------------------------------------
Revision : 5
Revision Date: Mon Aug 05 07:46:25 CEST 2019
User : Alice
Type : DEL
Employee : null null

Another useful clause is AuditEntity.revisionProperty, with which we can restrict the revisions to just those that are caused by changes from one particular user.

    query.add(AuditEntity.revisionProperty("username").eq("Bob"));
    results = query.getResultList();

MainQuery.java

"Bob" only updated one employee in revision 3

Revision : 3
Revision Date: Mon Aug 05 07:46:25 CEST 2019
User : Bob
Type : ADD
Employee : Robinson Janet

The last query method we take a look at in this blog post is forRevisionsOfEntityWithChanges(). This method works the same way as forRevisionsOfEntity(). The only difference is that this method returns a four-element array: entity class (0), revision entity (1), revision type (2), and a set of property names that changed in this revision (3).

You have to enable the withModifiedFlag flag (@Audited(withModifiedFlag = true)) if you want to use this query in your application.

    results = query.getResultList();
    for (Object[] result : results) {
      Employee employee = (Employee) result[0];
      CustomRevisionEntity revEntity = (CustomRevisionEntity) result[1];
      RevisionType revType = (RevisionType) result[2];
      Set<String> properties = (Set<String>) result[3];

      System.out.println("Revision     : " + revEntity.getId());
      System.out.println("Revision Date: " + revEntity.getRevisionDate());
      System.out.println("User         : " + revEntity.getUsername());
      System.out.println("Type         : " + revType);
      System.out.println("Changed Props: " + properties);
      System.out.println(
          "Employee     : " + employee.getLastName() + " " + employee.getFirstName());

      System.out.println("------------------------------------------------");
    }

MainQuery.java

Notice that the set with the changed properties only contains values when the revision is of type MOD (update).

Revision : 1
Revision Date: Mon Aug 05 07:46:25 CEST 2019
User : Alice
Type : ADD
Changed Props: []
Employee : Ralbern Michael
------------------------------------------------
Revision : 1
Revision Date: Mon Aug 05 07:46:25 CEST 2019
User : Alice
Type : ADD
Changed Props: []
Employee : Spencer Linda
------------------------------------------------
Revision : 3
Revision Date: Mon Aug 05 07:46:25 CEST 2019
User : Bob
Type : ADD
Changed Props: []
Employee : Robinson Janet
------------------------------------------------
Revision : 4
Revision Date: Mon Aug 05 07:46:25 CEST 2019
User : Alice
Type : MOD
Changed Props: [city, street]
Employee : Spencer Linda
------------------------------------------------
Revision : 5
Revision Date: Mon Aug 05 07:46:25 CEST 2019
User : Alice
Type : DEL
Changed Props: []
Employee : null null  

This concludes my overview of Envers.

See the official documentation to learn more about Envers: https://docs.jboss.org/hibernate/orm/5.4/userguide/html_single/Hibernate_User_Guide.html#envers

The source code presented this blog post is hosted on GitHub:
https://github.com/ralscha/blog2019/tree/master/envers