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 @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<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.
- set org.hibernate.envers.trackentitieschangedinrevision to true in Hibernate/JPA configuration. Hibernate will generate aREVCHANGES to record the change of the entity name.
- Create a custom revision entity that extends
org.hibernate.envers.DefaultTrackingModifiedEntitiesRevisionEntity
class.
@Entity @RevisionEntity public class ExtendedRevisionEntity extends DefaultTrackingModifiedEntitiesRevisionEntity { ... }
- In your custom revision entity, create a Set<String> 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<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.
评论