跳至主要内容

Auditing with Hibernate Envers

Auditing with Hibernate Envers

The approaches provided in JPA lifecyle hook and Spring Data auditing only track the creation and last modification info of an Entity, but all the modification history are not tracked.
Hibernate Envers fills the blank table.
Since Hibernate 3.5, Envers is part of Hibernate core project.

Configuration

Configure Hibernate Envers in your project is very simple, just need to add hibernate-envers as project dependency.
<dependency>
 <groupId>org.hibernate</groupId>
 <artifactId>hibernate-envers</artifactId>
</dependency>
Done.
No need extra Event listeners configuration as the early version.

Basic Usage

Hibernate Envers provides a simple @Audited annotation, you can place it on an Entity class or property of an Entity.
@Audited
private String description;
If @Audited annotation is placed on a property, this property can be tracked.
@Entity
@Audited
public class Signup implements Serializable {...}
If annotate @Audited with an Entity class, all properties of this Entity will be tracked.
Hibernate will generate extra tables for the audited Entities.
By default, an table name ended with _AUD will be generated for the related Entity.
For example, Conference_AUD will be generated for entityConference.
The audit table copies all audited fields from the entity table, and adds two fields, REVTYPE and REV, and the value of REVTYPE could be add, mod, del.
Besides these, an extra table named REVINFO will be generated by default, it includes two important fields, REV and REVTSTMP, it records the timestamp of every revision.
When you do some queries on the audited info of an entity, it will select the _AUD table join the REVINFO table.
The Envers events will be fired when the transaction is synchronized. This will cause an issue when use @Transactional annotation if there are some operations in a transaction.
@Test
// @Transactional
public void retrieveConference() {

 final Conference conference1 = newConference();

 Conference conf1 = transactionTemplate
   .execute(new TransactionCallback<Conference>() {

    @Override
    public Conference doInTransaction(TransactionStatus arg0) {
     conference1.setSlug("test-jud");
     conference1.setName("Test JUD");
     conference1.getAddress().setCountry("US");
     Conference reference = conferenceRepository
       .save(conference1);
     em.flush();
     return reference;
    }
   });

 // modifying description
 assertTrue(null != conf1.getId());
 final Conference conference2 = conferenceRepository
   .findBySlug("test-jud");

 log.debug("@conference @" + conference2);
 assertTrue(null != conference2);

 final Conference conf2 = transactionTemplate
   .execute(new TransactionCallback<Conference>() {

    @Override
    public Conference doInTransaction(TransactionStatus arg0) {
     conference2.setDescription("changing description...");
     Conference result = conferenceRepository
       .save(conference2);
     em.flush();
     return result;
    }
   });

 log.debug("@conf2 @" + conf2);
 // //modifying slug
 // conference.setSlug("test-jud-slug");
 // conference= conferenceRepository.save(conference);
 // em.flush();
 transactionTemplate.execute(new TransactionCallback<Conference>() {
  @Override
  public Conference doInTransaction(TransactionStatus arg0) {
   AuditReader reader = AuditReaderFactory.get(em);

   List<Number> revisions = reader.getRevisions(Conference.class,
     conf2.getId());
   assertTrue(!revisions.isEmpty());
   log.debug("@rev numbers@" + revisions);
   Conference rev1 = reader.find(Conference.class, conf2.getId(),
     2);

   log.debug("@rev 1@" + rev1);
   assertTrue(rev1.getSlug().equals("test-jud"));
   return null;
  }
 });

}
In this test method, TransactionTemplate is used to make the transaction be executed immediately.

Customize the revision info

Currently the REVINFO does not tracked the auditor of the certain revision, it is easy to customize the RevisionEntity to implement it.
Create a generic entity, add annotation @ResivionEntity.
@Entity
@RevisionEntity(ConferenceRevisionListener.class)
public class ConferenceRevisionEntity {
    @Id
    @GeneratedValue
    @RevisionNumber
    private int id;

    @RevisionTimestamp
    private long timestamp;
 
    @ManyToOne
    @JoinColumn(name="auditor_id")
    private User auditor;
}
A @RevisionNumber annotated property and a @RevisionTimestampannotated property are required. Hibernate provides a@MappedSuperclass DefaultRevisionEntity class, you can extend it directly.
@Entity
@RevisionEntity(ConferenceRevisionListener.class)
public class ConferenceRevisionEntity extends DefaultRevisionEntity{
 
 @ManyToOne
 @JoinColumn(name="auditor_id")
 private User auditor;
}
Create a custom RevisionListener class.
public class ConferenceRevisionListener implements RevisionListener {

 @Override
 public void newRevision(Object revisionEntity) {
  ConferenceRevisionEntity entity=(ConferenceRevisionEntity) revisionEntity;
  entity.setAuditor(SecurityUtils.getCurrentUser());
 }

}
When create a new revision, set the auditor info. In the real project, it could be principal info from Spring Security.
Now you can query the ConferenceRevisionEntity like a generic Entity, and get know who have modified the entity for some certain revisions.
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
 
 @Override
 protected void doInTransactionWithoutResult(TransactionStatus status) {
  AuditReader reader = AuditReaderFactory.get(em);

  List<Number> revisions = reader.getRevisions(Conference.class,
    conf2.getId());
  assertTrue(!revisions.isEmpty());
  log.debug("@rev numbers@" + revisions);
  
  ConferenceRevisionEntity entity=em.find(ConferenceRevisionEntity.class, revisions.get(0));
  
 
  log.debug("@rev 1@" + entity);
  assertTrue(entity.getAuditor().getId().equals(user.getId()));
  
 }
});

Tracking property-level modification

You can track which audited properties are modified at the certain revision.
There are tow options to enable tracking of property-level modification.
Add the following configuration in the Hiberante/JPA configuration to enable it globally, and modification of all properties annotated with @Audited will be tracked.
org.hibernate.envers.global_with_modified_flag true
Or specify a withModifiedFlag attribute of @Audited, for example@Audited(withModifiedFlag=true), which will track modification of all properties when annotate it on an entity class, or only track the specified property if annotate it on a property of an entity class.
Hibernate Envers will generate an extra _MOD field for every audited field in the _AUD table, which provides a flag for the audited field to identify it is modified at the certain revision.
@NotNull
@Audited(withModifiedFlag=true)
private String description;

@NotNull
@Audited(withModifiedFlag=true)
private String slug;
In the following test, create a Conferenc object, only change thedescription of the Conference, and verify the modification ofdescription and slug.
transactionTemplate.execute(new TransactionCallbackWithoutResult() {

 @Override
 protected void doInTransactionWithoutResult(TransactionStatus status) {
  AuditReader reader = AuditReaderFactory.get(em);

  List<Number> revisions = reader.getRevisions(Conference.class,
    conf2.getId());
  assertTrue(!revisions.isEmpty());
  log.debug("@rev numbers@" + revisions);
  List list = reader
    .createQuery()
    .forEntitiesAtRevision(Conference.class,
      revisions.get(0))
    .add(AuditEntity.id().eq(conf2.getId()))
    .add(AuditEntity.property("description").hasChanged())
    .getResultList();

  log.debug("@description list changed@" + list.size());
  assertTrue(!list.isEmpty());

  List slugList = reader
    .createQuery()
    .forEntitiesAtRevision(Conference.class,
      revisions.get(0))
    .add(AuditEntity.id().eq(conf2.getId()))
    .add(AuditEntity.property("slug").hasChanged())
    .getResultList();

  log.debug("@slugList 1@" + slugList.size());
  assertTrue(!slugList.isEmpty());

  list = reader
    .createQuery()
    .forEntitiesAtRevision(Conference.class,
      revisions.get(1))
    .add(AuditEntity.id().eq(conf2.getId()))
    .add(AuditEntity.property("description").hasChanged())
    .getResultList();

  log.debug("@description list changed@" + list.size());
  assertTrue(!list.isEmpty());

  slugList = reader
    .createQuery()
    .forEntitiesAtRevision(Conference.class,
      revisions.get(1))
    .add(AuditEntity.id().eq(conf2.getId()))
    .add(AuditEntity.property("slug").hasChanged())
    .getResultList();

  log.debug("@slugList 1@" + slugList.size());
  assertTrue(slugList.isEmpty());
 }
});

Tracking entity type modification

There are several ways to track the modification of the entity class.
  1. set org.hibernate.envers.trackentitieschangedinrevision to true in Hibernate/JPA configuration. Hibernate will generate aREVCHANGES to record the change of the entity name.
  2. Create a custom revision entity that extendsorg.hibernate.envers.DefaultTrackingModifiedEntitiesRevisionEntityclass.
 @Entity
 @RevisionEntity
 public class ExtendedRevisionEntity
             extends DefaultTrackingModifiedEntitiesRevisionEntity {
    ...
 }
 
  1. In your custom revision entity, create a Set<String> property, annotate it with @org.hibernate.envers.ModifiedEntityNamesannotation.
 @Entity
 @RevisionEntity
 public class ConferenceTrackingRevisionEntity {
    ...

    @ElementCollection
    @JoinTable(name = "REVCHANGES", joinColumns = @JoinColumn(name = "REV"))
    @Column(name = "ENTITYNAME")
    @ModifiedEntityNames
    private Set<String> modifiedEntityNames;
    
    ...
 }
 

A glance at Spring Data Envers project

There is an incubator project named Spring Data Envers under Spring Data which extends Spring Data JPA, and integrate Hibernate Envers with Spring Data JPA.
Add spring-data-envers as your project dependency.
<dependency>
 <groupId>org.springframework.data</groupId>
 <artifactId>spring-data-envers</artifactId>
 <version>0.2.0.BUILD-SNAPSHOT</version>
</dependency>
Set factory-class attribute to~.EnversRevisionRepositoryFactoryBean in <jpa-repositories/>. You have to use it to enable Spring Data Envers.
<jpa:repositories base-package="com.hantsylabs.example.spring.jpa"
  factory-class="org.springframework.data.envers.repository.support.EnversRevisionRepositoryFactoryBean"></jpa:repositories>
There is an extra ResivionRepository interface from Spring Data Commons which provides the capability of querying the entity revision info.
public interface RevisionRepository<T, ID extends Serializable, N extends Number & Comparable<N>> {

    Revision<N, T> findLastChangeRevision(ID id);

    Revisions<N, T> findRevisions(ID id);

    Page<Revision<N, T>> findRevisions(ID id, Pageable pageable);
}
Revision and Revisions are from Spring Data Commons. The former envelopes the revision info, such as revision number, revision date, and related Entity data snapshot. The later is an iterator of theRevision.
Make your repository extend RevisionRepository.
@Repository
public interface SignupRepository extends RevisionRepository<Signup, Long, Integer>,
  JpaRepository<Signup, Long>{

 Signup findByConference(Conference conference);

 Signup findById(Long id);

}
Have a try now.
@Test
// @Transactional
public void retrieveSignupRevision() {

 final Signup signup = newSignup();

 final Signup signup2 = transactionTemplate
   .execute(new TransactionCallback<Signup>() {

    @Override
    public Signup doInTransaction(TransactionStatus arg0) {
     signupRepository.save(signup);
     em.flush();
     return signup;
    }
   });

 // modifying description
 assertTrue(null != signup2.getId());

 log.debug("@Signup @" + signup2);
 assertTrue(null != signup2);

 transactionTemplate.execute(new TransactionCallbackWithoutResult() {

  @Override
  protected void doInTransactionWithoutResult(TransactionStatus status) {
   Revisions<Integer, Signup> revision = signupRepository
     .findRevisions(signup2.getId());

   assertTrue(!revision.getContent().isEmpty());

   Revision<Integer, Signup> lastRevision = signupRepository
     .findLastChangeRevision(signup2.getId());
   
   assertTrue(lastRevision.getRevisionNumber()==1);

  }
 });

}

Sample codes

The codes are hosted on my github.com account.

评论

此博客中的热门博文

AngularJS CakePHP Sample codes

Introduction This sample is a Blog application which has the same features with the official CakePHP Blog tutorial, the difference is AngularJS was used as frontend solution, and CakePHP was only use for building backend RESR API. Technologies AngularJS   is a popular JS framework in these days, brought by Google. In this example application, AngularJS and Bootstrap are used to implement the frontend pages. CakePHP   is one of the most popular PHP frameworks in the world. CakePHP is used as the backend REST API producer. MySQL   is used as the database in this sample application. A PHP runtime environment is also required, I was using   WAMP   under Windows system. Post links I assume you have some experience of PHP and CakePHP before, and know well about Apache server. Else you could read the official PHP introduction( php.net ) and browse the official CakePHP Blog tutorial to have basic knowledge about CakePHP. In these posts, I tried to follow the steps describ

JPA 2.1: Attribute Converter

JPA 2.1: Attribute Converter If you are using Hibernate, and want a customized type is supported in your Entity class, you could have to write a custom Hibernate Type. JPA 2.1 brings a new feature named attribute converter, which can help you convert your custom class type to JPA supported type. Create an Entity Reuse the   Post   entity class as example. @Entity @Table(name="POSTS") public class Post implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name="ID") private Long id; @Column(name="TITLE") private String title; @Column(name="BODY") private String body; @Temporal(javax.persistence.TemporalType.DATE) @Column(name="CREATED") private Date created; @Column(name="TAGS") private List<String> tags=new ArrayList<>(); } Create an attribute convert