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>
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;
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;
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" />
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.
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:
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;
}
}
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();
}
}
With this configuration in place, the REVINFO table now contains a new field: username
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();
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();
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();
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();
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();
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);
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);
With getRevisionDate()
we have access to the revision date (REVINFO.TIMESTAMP).
Date revisionDate = reader.getRevisionDate(rev);
System.out.println(revisionDate);
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);
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);
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
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
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
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);
// 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));
// 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"),
// 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
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
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
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
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("------------------------------------------------");
}
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();
"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("------------------------------------------------");
}
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