Core Java

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 to CustomRevision, which is our custom revision entity.
  • The username field of CustomRevision 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.

Yatin Batra

An experience full-stack engineer well versed with Core Java, Spring/Springboot, MVC, Security, AOP, Frontend (Angular & React), and cloud technologies (such as AWS, GCP, Jenkins, Docker, K8).
Subscribe
Notify of
guest


This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button