Auditing with Hibernate Envers

Auditing with Hibernate Envers

The jpa lifecyle hook method and Spring Data auditing only store 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 project is very simple, add hibernate-envers as a project dependency.

 
    
 
  
 
  
    org.hibernate 
  
    
 
  
 
  
    hibernate-envers 
  


 

 

It is 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;

When an @Audited annotation is placed on a property, this property can be tracked.

@Entity
@Audited
public class Signup implements Serializable {...}

If annotated @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 entity Conference.

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 on a method.

@Test
// @Transactional
public void retrieveConference() {

    final Conference conference1 = newConference();

    Conference conf1 = transactionTemplate
            .execute(new TransactionCallback

 

 
  
 
  
 () {

                @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
 
  
 
  
    () { @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 
   
     () { @Override public Conference doInTransaction(TransactionStatus arg0) { AuditReader reader = AuditReaderFactory.get(em); List 
    
      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, I use TransactionTemplate to make the transaction happens 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 @RevisionTimestamp annotated 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

 

 
  
 
  
  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;

Only change the description of the Conference, and verify the modification of description and slug.

transactionTemplate.execute(new TransactionCallbackWithoutResult() {

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

        List

 

 
  
 
  
  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.track_entities_changed_in_revision to true in Hibernate/JPA configuration. Hibernate will generate a REVCHANGES to record the change of the entity name.

  2. Create a custom revision entity that extends org.hibernate.envers.DefaultTrackingModifiedEntitiesRevisionEntity class.

    @Entity
    @RevisionEntity
    public class ExtendedRevisionEntity
             extends DefaultTrackingModifiedEntitiesRevisionEntity {
    ...
    }
  3. In your custom revision entity, create a Set property, annotate it with @org.hibernate.envers.ModifiedEntityNames annotation.

    @Entity
    @RevisionEntity
    public class ConferenceTrackingRevisionEntity {
    ...
    
    @ElementCollection
    @JoinTable(name = "REVCHANGES", joinColumns = @JoinColumn(name = "REV"))
    @Column(name = "ENTITYNAME")
    @ModifiedEntityNames
    private Set
    
       
    
       
        
     
        
      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.

 
    
 
  
 
  
    org.springframework.data 
  
    
 
  
 
  
    spring-data-envers 
  
    
 
  
 
  
    0.2.0.BUILD-SNAPSHOT 
  


 

 

Specify the a factory-class attribute in * , you have to use it to enable Spring Data Envers.

 

There is an extra ResivionRepository interface from Spring Data Commons provides you the capability of querying the entity revision info.

public interface RevisionRepository

 

 
  
 
  
 > {

    Revision
 
  
 
  
    findLastChangeRevision(ID id); Revisions 
   
     findRevisions(ID id); Page 
    
      > findRevisions(ID id, Pageable pageable); } 
     
    
  

 

 

Make your repository extend it.

@Repository
public interface SignupRepository extends RevisionRepository

 

 
  
 
  
 ,
        JpaRepository
 
  
 
  
    { 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

 

 
  
 
  
 () {

                @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
 
  
 
  
    revision = signupRepository .findRevisions(signup2.getId()); assertTrue(!revision.getContent().isEmpty()); Revision 
   
     lastRevision = signupRepository .findLastChangeRevision(signup2.getId()); assertTrue(lastRevision.getRevisionNumber()==1); } }); } 
    
  

 

 

Sample codes

The codes are hosted on my github.com account.

https://github.com/hantsy/spring-sandbox

你可能感兴趣的:(spring,spring,Hibernate,jpa,Data,Envers)