Hibernate Envers: Simple Implementations (Part 1)
Look at an introduction to Hibernate Envers and see a simple implementation.
Join the DZone community and get the full member experience.
Join For FreeIntroduction
Auditing is an important part of software applications. Almost every business domain requires an audit log to manage the changes of acquired data. More than that, auditing is also required to keep applications safe from fraudulent and unethical access. Many applications also check the changelog of data for their internal processes. Many Java-based software implement triggers on the database layer for auditing, but Hibernate gives a more convenient way to implement auditing.
Hibernate Envers: Features
Hibernate Envers is a framework for auditing. Though Hibernate is an ORM technology, auditing tasks based on Hibernate entities means changes on the entity is audited and saved on the database. Auditing of all mappings is defined by the JPA specification. Revision of each entity log is saved by Hibernate Envers. Hibernate Envers gives the way to read historical data log.
Implementation
It's built on top of Hibernate and Hibernate JPA. As Hibernate is an ORM technology and Hibernate Envers also entity based, we must focus on the entity. As we are using Hibernate Envers with a spring maven project, we have to specify this library on pom.xml file. Others Spring and Hibernate jars are specified in pom.xml.
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-envers</artifactId>
<version>5.4.0.Final</version>
</dependency>
An entity is an object that represents the tables of a database. And Envers is using Hibernate's event system. Entity transitions are intercepted by the Hibernate audit system and audits them afterward. So, we will create two entities for our implementation; Employee entity and Department entity.
Employee Entity:
@Entity
@Audited
public class Employee implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
String name;
String address;
String email;
@ManyToOne
Department department;
public Integer getVersion() {
return version;
}
public void setVersion(Integer version) {
this.version = version;
}
@Version
private Integer version;
public Department getDepartment() {
return department;
}
public void setDepartment(Department department) {
this.department = department;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
@CreatedBy
private String createdBy;
public String getCreatedBy() {
return createdBy;
}
public void setCreatedBy(String createdBy) {
this.createdBy = createdBy;
}
@LastModifiedBy
private String modifiedBy;
public String getModifiedBy() {
return modifiedBy;
}
public void setModifiedBy(String modifiedBy) {
this.modifiedBy = modifiedBy;
}
}
Department Entity:
@Entity
@Audited
public class Department {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
String name;
String responsibility;
@LastModifiedBy
private String modifiedBy;
@CreatedBy
private String createdBy;
@Version
private Integer version;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getResponsibility() {
return responsibility;
}
public void setResponsibility(String responsibility) {
this.responsibility = responsibility;
}
public Integer getVersion() {
return version;
}
public void setVersion(Integer version) {
this.version = version;
}
public String getCreatedBy() {
return createdBy;
}
public void setCreatedBy(String createdBy) {
this.createdBy = createdBy;
}
public String getModifiedBy() {
return modifiedBy;
}
public void setModifiedBy(String modifiedBy) {
this.modifiedBy = modifiedBy;
}
}
Here, we just added an @Audited annotation at the top of the entity, which will identify the entities to be audited or not. Though we are using a spring maven project with a java-based configuration, we have to define audit-related properties on the application config file.
@Configuration
@ComponentScan(basePackages = {"com.sanju.envers","com.sanju.envers.repository"})
@EnableTransactionManagement
@EnableJpaRepositories(entityManagerFactoryRef = "emf",basePackages = {"com.sanju.envers.repository"})
@EnableAsync
@EnableJpaAuditing
public class AppConfig {
@Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.mariadb.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/envers_practice?autoReconnect=true");
dataSource.setUsername("root");
dataSource.setPassword("");
return dataSource;
}
@Bean
public LocalContainerEntityManagerFactoryBean emf(){
HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
adapter.setDatabasePlatform("org.hibernate.dialect.MariaDB10Dialect"); //you can change this if you have a different DB
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
Properties properties = new Properties();
properties.put("hibernate.dialect", MariaDB10Dialect.class);
properties.put(AvailableSettings.HBM2DDL_AUTO, SchemaAutoTooling.UPDATE.name().toLowerCase());
properties.put(AvailableSettings.SHOW_SQL,"true");
properties.put("org.hibernate.envers.audit_table_suffix",
"_aud");
properties.put("hibernate.listeners.envers.autoRegister",true);
properties.put("hibernate.envers.autoRegisterListeners",true);
factory.setJpaProperties(properties);
factory.setJpaVendorAdapter(adapter);
factory.setDataSource(dataSource());
factory.setPackagesToScan("com.sanju.envers");
factory.setSharedCacheMode(SharedCacheMode.ENABLE_SELECTIVE);
factory.setValidationMode(ValidationMode.NONE);
return factory;
}
@Bean
public PlatformTransactionManager transactionManager()
{
return new JpaTransactionManager(
this.emf().getObject());
}
@Bean
public TaskExecutor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(6);
executor.setThreadNamePrefix("default_task_executor_thread");
executor.initialize();
return executor;
}
}
Here, we annotated this class with an @EnableJpaAuditing for enabling JPA Hibernate auditing and defined datasources, transactional properties, and threadpool properties. We configured EntityManager, and here, we define some Hibernate Envers properties.
properties.put("org.hibernate.envers.audit_table_suffix","_aud");
properties.put("hibernate.listeners.envers.autoRegister",true);
properties.put("hibernate.envers.autoRegisterListeners",true);
The first one defines that when we run this project, it will create or update an audit table for auditing with suffix _aud. And the last two properties define that the Envers listener will be used by a default listener or custom listener. Revinfo default table contains two properties (rev, revtstmp), which will define the revision of the entity in audit tables.
Please clone the project from here and change the properties hibernate.listeners.envers.autoRegister and hibernate.envers.autoRegisterListeners to true at AppConfig.java. Resolve the maven dependency and then run Application.java. Before that, change your database configuration to AppConfig.java.
@Beanpublic DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.mariadb.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/envers_practice?autoReconnect=true");
dataSource.setUsername("root");
dataSource.setPassword("");
return dataSource;
}
I am using MariaDB. You can choose your favourite database, then you have to change driver classes. Also, change on pom.xml.
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<version>2.4.0</version>
</dependency>
Now create an empty database named by envers_practice. Now run Application.java file. Go to the database and you can see that the audit table and revision table are created.
Now we can think about the custom listener. Sometimes, we need to make some custom operations during auditing. Then, we need to add a Custom listener to the application. Before that, we will change the properties hibernate.listeners.envers.autoRegister and hibernate.envers.autoRegisterListeners to false at AppConfig.java. And then look at the Custom listener configuration:
@Component
public class EnversListenerConfiguration {
@Autowired
EntityManagerFactory entityManagerFactory;
@PostConstruct
protected void init() {
SessionFactoryImpl sessionFactory = entityManagerFactory.unwrap(SessionFactoryImpl.class);
EventListenerRegistry registry = sessionFactory.getServiceRegistry().getService(EventListenerRegistry.class);
EnversService enversService = sessionFactory.getServiceRegistry().getService(EnversService.class);
registry.getEventListenerGroup(EventType.PRE_INSERT).appendListener(new CustomEnversPreInsertEventListenerImpl());
registry.getEventListenerGroup(EventType.PRE_UPDATE).appendListener(new CustomEnversPreUpdateEventListenerImpl(enversService));
registry.getEventListenerGroup(EventType.POST_UPDATE).appendListener(new CustomPostUpdateEventListener(enversService));
registry.getEventListenerGroup(EventType.PRE_COLLECTION_UPDATE).appendListener(new CustomPreUpdateCollectionListener(enversService));
registry.getEventListenerGroup(EventType.POST_COLLECTION_UPDATE).appendListener(new CustomPostUpdateCollectionListener());
}
}
Then the program can perform its necessary operations during different auditing events.
Read Audited Data
Hibernate Envers has some interfaces to read audit data. AuditReader reads audit logs of entities. We get AuditReader from AuditReaderFactory by using entitymanager. AuditQuery is also an interface that can read all audited data with revision information for a specific entity. Here, Object[] contains three pieces of audit information. First, the array element contains original tables or entities changes, second, the element of the array contains DefaultRevisionEntity, and the third one contains RevisionType, which means operation type.
private static <T> void auditDataRead(Class<T> tClass, AnnotationConfigApplicationContext configApplicationContext) {
System.out.println("Reading auditing data...");
EntityManagerFactory emf = configApplicationContext.getBean(EntityManagerFactory.class);
AuditReader auditReader = AuditReaderFactory.get(emf.createEntityManager());
AuditQuery query = auditReader.createQuery()
.forRevisionsOfEntity(tClass, false, true);
List<Object[]> resultList = query.getResultList();
resultList.forEach(objects -> {
System.out.println("------------------------------------------------------------------------\n");
if (Employee.class.equals(tClass)) {
Employee employeeRev = (Employee) objects[0];
System.out.println("Employee info : ");
System.out.println("Id : " + employeeRev.getId());
System.out.println("Name : " + employeeRev.getName());
System.out.println("Email : " + employeeRev.getEmail());
System.out.println("Address : " + employeeRev.getAddress());
} else if (Department.class.equals(tClass)) {
System.out.println("Department info: ");
Department department = (Department) objects[0];
System.out.println("Id : "+department.getId());
System.out.println("Responsibility : "+department.getResponsibility());
System.out.println("Name : "+department.getName());
}
DefaultRevisionEntity revisionEntity = (DefaultRevisionEntity) objects[1];
System.out.println("Revision : " + revisionEntity.getId());
System.out.println("Date : " + revisionEntity.getRevisionDate());
RevisionType revisionType = (RevisionType) objects[2];
System.out.println("Operation : " + revisionType.name());
});
}
This was an overview of Hibernate Envers. I will discuss it more in my next article.
Thanks. You can find the code from this article here.
Published at DZone with permission of Sanjoy Kumer Deb. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments