Hibernate Envers – Extending Revision Info with Custom Fields
Hibernate Envers provides automatic auditing of entity changes in Hibernate-based applications. By default, Envers tracks entity modifications with revision numbers and timestamps. However, in many real-world applications, it is useful to extend revision metadata with additional custom fields like the username of the person making the change. Let us delve into understanding Java Hibernate Envers and how it helps in extending revision metadata with custom fields for better auditing.
1. Project Setup
To get started, add the required dependencies in your pom.xml
file:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 | < dependency > < groupId >org.hibernate</ groupId > < artifactId >hibernate-envers</ artifactId > < version >your__jar__latest__version</ version > </ dependency > < dependency > < groupId >org.springframework.boot</ groupId > < artifactId >spring-boot-starter-data-jpa</ artifactId > </ dependency > < dependency > < groupId >com.h2database</ groupId > < artifactId >h2</ artifactId > < scope >runtime</ scope > </ dependency > |
We are using Hibernate Envers for auditing, Spring Boot JPA for persistence, and H2 as our in-memory database.
2. Department Example
We will create an application where a department manages department records. Each modification to a department’s details will be logged using Hibernate Envers.
2.1 Domain Layer
Define the Department
entity and enable auditing by annotating it with @Audited
:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 | import javax.persistence.*; import org.hibernate.envers.Audited; @Entity @Audited public class Department { @Id @GeneratedValue (strategy = GenerationType.IDENTITY) private Long id; private String name; private String type; // Getters and Setters } |
2.1.1 Code Explanation
The Department
class is a simple Java entity representing a department in our system. It is annotated with @Audited
, which enables Hibernate Envers to track changes made to this entity. This means that any insert, update, or delete operation performed on a Department
record will be recorded in an audit table. The class contains three fields:
id
: A unique identifier for each department. It is marked with@Id
, indicating it as the primary key. The@GeneratedValue(strategy = GenerationType.IDENTITY)
annotation ensures that the ID is auto-generated by the database.name
: A string field representing the department’s name.type
: A string field representing the type of the department (e.g., hr, engineering, technology, operations, etc).
Additionally, the class includes getter and setter methods (not shown in the code) to allow access and modification of the entity’s properties. With Hibernate Envers enabled, any changes to a Department
object will be stored in an audit table, allowing historical tracking of modifications.
2.2 Repository Layer
Create a repository interface to perform CRUD operations on the Department entity:
1 2 3 4 5 6 | import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface DepartmentRepository extends JpaRepository<Department, Long> { } |
2.2.1 Code Explanation
The DepartmentRepository
interface is a Spring Data JPA repository that provides CRUD (Create, Read, Update, Delete) operations for the Deartment
entity. It is annotated with @Repository
, which marks it as a Spring-managed component responsible for data access logic. This interface extends JpaRepository<Deartment, Long>
, which is a predefined Spring Data JPA interface. By extending JpaRepository
, the repository inherits several built-in methods for database operations, such as:
save(Deartment department)
– Saves or updates a department record.findById(Long id)
– Retrieves a department by its ID.findAll()
– Fetches all department records from the database.deleteById(Long id)
– Deletes a department record by ID.
By leveraging Spring Data JPA, we avoid writing boilerplate code for basic database operations, making our application more concise and maintainable. Since Hibernate Envers is enabled for the Department
entity, any changes made through this repository will automatically be tracked in the audit history.
2.3 CustomRevisionListener Implementation
To track additional fields like username
, we create a custom revision entity. This entity extends the standard Envers revision metadata.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 | import org.hibernate.envers.RevisionEntity; import org.hibernate.envers.RevisionNumber; import org.hibernate.envers.RevisionTimestamp; import javax.persistence.*; import java.util.Date; @Entity @RevisionEntity (CustomRevisionListener. class ) public class CustomRevision { @Id @GeneratedValue (strategy = GenerationType.IDENTITY) @RevisionNumber private int id; @RevisionTimestamp private Date timestamp; private String username; // Getters and Setters } |
2.3.1 Custom Revision Listener
The listener class captures additional audit information like the username of the person making the change.
1 2 3 4 5 6 7 8 9 | import org.hibernate.envers.RevisionListener; public class CustomRevisionListener implements RevisionListener { @Override public void newRevision(Object revisionEntity) { CustomRevision customRevision = (CustomRevision) revisionEntity; customRevision.setUsername( "admin" ); // Fetch from Security Context } } |
Make note that in a real-world application, we would retrieve the currently logged-in user from Spring Security.
2.3.1.1 Code Explanation
The CustomRevisionListener
class implements the RevisionListener
interface, which is part of Hibernate Envers. This listener is responsible for customizing the audit revision process by adding additional metadata whenever a revision is created. The class overrides the newRevision(Object revisionEntity)
method, which is triggered whenever an entity is modified, and a new revision is recorded. Inside this method:
- The incoming
revisionEntity
object is cast toCustomRevision
, which is our custom revision entity. - The
username
field ofCustomRevision
is set to"admin"
. In a real-world application, this value should be dynamically fetched from the security context to record the actual user making the change.
This approach allows us to extend the default Hibernate Envers revision tracking by storing additional information, such as the user responsible for modifications. By implementing a custom revision listener, we enhance auditing capabilities, making it more useful for security and compliance purposes.
2.4 Service Layer
Implement the service to manage department records.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 | import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class DepartmentService { private final DepartmentRepository departmentRepository; public DepartmentService(DepartmentRepository departmentRepository) { this .departmentRepository = departmentRepository; } @Transactional public Department saveDept(Department department) { return departmentRepository.save(department); } } |
2.4.1 Code Explanation
The DepartmentService
class is a service layer component responsible for handling business logic related to department management. It is annotated with @Service
, which marks it as a Spring-managed service, making it available for dependency injection throughout the application.
This class contains a reference to DepartmentRepository
, which is injected through the constructor. Using constructor-based injection ensures that the repository dependency is properly initialized when an instance of DepartmentService
is created.
The saveDept(Department dept)
method is annotated with @Transactional
, which ensures that the database operation is executed within a transaction. This method performs the following actions:
- Accepts a
Department
object as a parameter. - Calls
departmentRepository.save(department)
to persist or update the department record in the database. - Returns the saved
Department
object.
Since Hibernate Envers is enabled for the Department
entity, every time a department record is created or updated using this service, an audit entry is automatically generated, tracking the changes. This service layer helps in abstracting database interactions and provides a cleaner architecture by separating business logic from data access logic.
2.5 Testing
To verify that our auditing is working correctly, we will create a test case that inserts and updates a department record.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 | import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest public class DepartmentAuditTest { @Autowired private DepartmentService deptService; @Test public void testAudit() { Department department = new Department(); deptService.setName( "RnD" ); deptService.setType( "Engineering" ); deptService.saveDept(department); } } |
This test inserts a department record into the department
table, updates it, and simultaneously triggers event auditing, recording the changes in the audit
table.
2.6 Retrieving Audit Data
Hibernate Envers allows retrieving audit history using the AuditReader
:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 | import org.hibernate.envers.AuditReader; import org.hibernate.envers.AuditReaderFactory; import org.springframework.stereotype.Service; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import java.util.List; @Service public class AuditService { @PersistenceContext private EntityManager entityManager; public List<Number> getRevisions(Long departmentId) { AuditReader auditReader = AuditReaderFactory.get(entityManager); List<Number> revisions = auditReader.getRevisions(Department. class , departmentId); System.out.println(revisions); return revisions; } } |
The above code retrieves all revision numbers for a given department ID, and in our case, it returns 1. This output signifies that the department record with ID 1 has undergone a single revision, representing its initial creation. Note that we are only fetching the revision numbers from the database to adhere to best SQL practices, as retrieving unnecessary data is inefficient and considered poor coding practice.
2.6.1 Get all Revisions – Optional
Consider that if you need the complete data for all revisions of a specific department, you can use the Hibernate Envers’ find()
method.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | import org.hibernate.envers.AuditReader; import org.hibernate.envers.AuditReaderFactory; import org.springframework.stereotype.Service; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import java.util.List; import java.util.stream.Collectors; @Service public class AuditService { @PersistenceContext private EntityManager entityManager; public List<Department> getDepartmentRevisions(Long departmentId) { AuditReader auditReader = AuditReaderFactory.get(entityManager); // Fetch all revision numbers for the given department List<Number> revisions = auditReader.getRevisions(Department. class , departmentId); // Retrieve full data for each revision return revisions.stream() .map(rev -> auditReader.find(Department. class , departmentId, rev)) .collect(Collectors.toList()); } } |
2.6.1.1 Code Explanation
The AuditService
class provides an additional method getDepartmentRevisions(...)
to retrieve all historical revisions of a specific Department
entity using Hibernate Envers. It uses an EntityManager
with @PersistenceContext
to interact with the database. The getDepartmentRevisions(Long departmentId)
method first obtains an AuditReader
instance from AuditReaderFactory
, then fetches all revision numbers for the given department using getRevisions()
. It then iterates over these revisions, using auditReader.find()
to retrieve the full entity data for each revision, and return the list of historical versions of the Department
entity.
2.7 Querying the Revision Table
The revision table stores historical data for audited entities. You can retrieve revision metadata using the following SQL queries:
1 2 3 4 5 6 7 8 | -- Get all revisions for an entity SELECT revt.REV, revt.REVTSTMP, revt.USERNAME, department_aud. NAME , department_aud_aud.TYPE FROM department_aud_AUD department_aud_aud JOIN REVINFO revt ON department_aud_aud.REV = revt.REV WHERE department_aud_aud.ID = ?; -- Get a specfic revision of an entity SELECT * FROM department_aud_AUD WHERE ID = ? AND REV = ?; |
Using these queries, you can track changes made to an entity and get historical values for debugging or compliance purposes.
3. Conclusion
In this article, we explored how to use Hibernate Envers to track entity modifications and extended the auditing mechanism to capture additional information, such as the username of the person making changes. This approach is particularly useful in applications requiring compliance, historical tracking, or security audits. As next steps, we can further enhance this implementation by integrating it with Spring Security to dynamically fetch the logged-in user, exposing auditing data via REST APIs, and leveraging audit tables for reporting purposes.