In any application, auditing means that we track and log every change in all business objects, i.e, track each insert, update and delete operations.

Basically it involves tracking three things -

  1. What operation was performed?
  2. Who did it?
  3. When was it done?

Auditing helps us in maintaining history records, which can later help us in tracking user activities.

Hibernate Envers is a project that seeks to make auditing persistent classes simple. You only need to annotate your persistent class or a few of its attributes with @Audited if you wish to audit them. A table will be generated that contains the history of the changes made to each audited entity. Then, retrieving and analysing past data won’t take much work.

Process To Get Started off with Envers

  1. The first and the foremost step is to have the dependency in place for the application
     implementation group: 'org.springframework.data', name: 'spring-data-envers', version: '2.6.2
    
  2. Following this we will need a set of well defined persistent entities in the application who’s data is crucial and all operations on these entities will need to be verified at any point in time. For example -
     @Entity
     @Table(name = "user")
     public class User {
    
             //list of attributes
    			 
             //getters
    			 
             //setters
    			 
             }    
    
  3. In order to enable auditing for this table we need to add the annotation @Audited on the table that is required to be tracked. This annotation will generate the audit table in the database that will record all the changes done to this entity. You can either have this annotation at entity level, which means that changes to all the attributes of that entity will be tracked, or you can have it at the attribute level which means that only that attribute changes will be tracked. For attributes that you do not require to be tracked, we can have the @NotAudited annotation on the attribute.

  4. The @Audited has a flag ( withModifiedFlag ) which could be toggled to note the change in values to the attribute in the audit table (all attributes if at entity level, or only specified attribute if at attribute level). This flag is of boolean type which is true for an attribute when the value for that attribute of that entity has changed else false.
    @Entity
    @Audited //Entity level Auditing
    @Table(name = "user")
    public class User {
        
    //list of attributes
        
    //getters
        
    //setters
        
    }
        
    // (OR)
        
    @Entity
    @Table(name = "user")
    public class User {
        
    @Audited //Attribute level audting
    private String name;
       
    //list of attributes
        
    //getters
        
    //setters
        
    }
    
  5. To capture who created the record and when the record was created we need a set of attributes, for which JPA provides a way to capture these values using custom auditing annotations. In order for the auditing annotations to work we need to enable the entity to listen to events that change the records in the table. The attributes required are as follows -
    • createdBy
    • createdDate
    • modifiedBy
    • modifiedDate
  6. These annotations ( @CreatedBy,@LastModifiedBy,@CreatedDate,@LastModifiedDate ) help to capture the auditor from security context of Spring and the time of change.
    @Entity
    @Audited
    @EntityListeners(AuditingEntityListener.class) // enables the entity to listen to any audits
    @Table(name = "user")
    public class User {
       
    //list of attributes
       
    @CreatedBy
    private String createdBy;
       
    @LastModifiedBy
    private String lastModifiedBy;
       
    @CreatedDate
    private Date createdDate;
       
    @LastModifiedDate
    private Date lastModifiedDate;
       
    //getters
       
    //setters
       
    }
    
  7. We can write custom AuditAwareListeners in order to capture the auditor name using custom logic.
    public class CustomAuditAwareListener implements AuditorAware<String> {
       
     @Override
     public Optional<String> getCurrentAuditor() {
             
         //logic to retrieve username using custom logic
     }
    }
    

Configuration

Since Envers generates some entities and maps them to tables, it is possible to set the names of the fields that are generated as well as the prefix and suffix that are added to the entity name to construct an audit table for an entity.

The list of properties that could be edited are as follows -

Property Name Default Value Description
org.hibernate.envers.audit_table_prefix   String that will be prepended to the name of an audited entity to create the name of the entity, that will hold audit information
org.hibernate.envers.audit_table_suffix _AUD String that will be appended to the name of an audited entity to create the name of the entity, that will hold audit information. If you audit an entity with a table name Person, in the default setting Envers will generate a Person_AUD table to store historical data.
org.hibernate.envers.revision_field_name REV Name of a field in the audit entity that will hold the revision number.
org.hibernate.envers.revision_type_field_name REVTYPE Name of a field in the audit entity that will hold the type of the revision (currently, this can be: add, mod, del).
org.hibernate.envers.revision_on_collection_change true Should a revision be generated when a not-owned relation field changes (this can be either a collection in a one-to-many relation, or the field using “mappedBy” attribute in a one-to-one relation).
org.hibernate.envers.do_not_audit_optimistic_locking_field true When true, properties to be used for optimistic locking, annotated with @Version, will be automatically not audited (their history won’t be stored; it normally doesn’t make sense to store it).
org.hibernate.envers.store_data_at_delete false Should the entity data be stored in the revision when the entity is deleted (instead of only storing the id and all other properties as null). This is not normally needed, as the data is present in the last-but-one revision. Sometimes, however, it is easier and more efficient to access it in the last revision (then the data that the entity contained before deletion is stored twice).
org.hibernate.envers.default_schema null (same as normal tables) The default schema name that should be used for audit tables. Can be overriden using the @AuditTable(schema="...") annotation. If not present, the schema will be the same as the schema of the normal tables.
org.hibernate.envers.default_catalog null (same as normal tables) The default catalog name that should be used for audit tables. Can be overriden using the @AuditTable(catalog="...") annotation. If not present, the catalog will be the same as the catalog of the normal tables.

The required properties can be set in the application.yml file and the audit entities generated will be user driven and more meaningful as per the industry standards.

We will need to even enable the application to support envers repository and JPA Auditing fo which we will need to configure as follows:

@SpringBootApplication
@EnableJpaAuditing(auditorAwareRef="auditorAware") //auditorAwareRef points to the name of the bean created for custom auditing listener
@EnableEnversRepositories(repositoryFactoryBeanClass = EnversRevisionRepositoryFactoryBean.class)
public class Application {
 
    public static void main(String[] args) {
        SpringApplication.run(FleetAdminApplication.class, args);
    }
 
    @Bean //can be defined in a config
    public AuditorAware<String> auditorAware() {
        return new CustomAuditAwareListener();
    }
}

Querying Data

Now that we have the entire history of all types of operations that have taken place on the audited entities, we will need to find a way to query the data in such a way that provides a meaningful information and quickly deduct what changes have been done, what data have been modified and to what was the data modified to.

We will make use of the SQL lag() function for this purpose that helps provide the above mentioned data to us, making it useful for any business fraction of the company to read and identify the changes.

An example SQL query is as follows:

SELECT id, value, lag(value) over (order by id) as previous_value FROM table_name where id=:id;

We can provide an API that lists the changes that have been made by creating a native query in a similar fashion. We need to create a POJO that contains all the information that we need and create a mapping with the results of the native query and return a meningful response as part of a UI call which would make more sense to a User to understand the auditing done by far on the entity.

@NamedNativeQuery(
        name = "user_history_query",
        query ="SELECT id, tech, lag(tech) over (order by rev) as previous_tech, name, lag(name) over (order by rev) as previous_name, rev, revtype FROM user_aud where id=:id",
        resultSetMapping = "user_history_dto"
)
@SqlResultSetMapping(
        name = "user_history_dto",
        classes = @ConstructorResult(
                targetClass = AuditEntity.class,
                columns = {
                        @ColumnResult(name = "id", type = Long.class),
                        @ColumnResult(name = "tech", type = String.class),
                        @ColumnResult(name = "previous_tech", type = String.class),
                        @ColumnResult(name = "name", type = String.class),
                        @ColumnResult(name = "previous_name", type = String.class),
                        @ColumnResult(name = "rev", type = Integer.class),
                        @ColumnResult(name = "revtype", type = Integer.class)
                }
        )
)
@Entity
@Audited
@Table(name = "user")
public class User {
 
//list of attributes
 
//getters
 
//setters
 
}

//(Repository Crud function)
public class Repositoy {
   @Query(name = "user_history_query", nativeQuery = true)
   List<AuditEntity> findUserHistory(@Param("id") Long id);
}
 
//(POJO you wish to map the results to)
 
public class AuditEntity {
 
    private Long id;
    private String tech;
    private String previousTech;
    private String name;
    private String previousName;
    private int rev;
    private int revType;
 
    //constructors
 
    //getters
 
    //setters
 
 }

Screenshots

  • User Table
    image: /assets/hibernate-envers/user-table.png

  • Revision Info Table
    image: /assets/hibernate-envers/rev-info-table.png

  • User Audit Table
    image: /assets/hibernate-envers/user-audit-table.png

  • Query Fetching changes done for a particular user
    image: /assets/hibernate-envers/query.png

References