Hibernate Envers – Tracking entity names modified during revisions (HHH-5580)
07/02/2011 9 Comments
Hibernate Envers is an open-source project that provides historical versioning of application’s entity data. It is based on the idea of entity revisions, which shall be intuitive for every user of source control management tools. Since the release 4.0.0.Beta1 of Hibernate Core, Envers enables users to retrieve modifications applied exactly at a specified revision number without defining concrete entity type. This feature has been requested several times on Hibernate forum (for example here) and reported as a major improvement in the JIRA system (HHH-5580).
By default, Envers does not track entity types that have been modified in each revision. This implies the necessity to query all tables storing audited data in order to retrieve changes made during given revision [1]. While designing the patch for HHH-5580 issue, me and Adam Warski agreed to add an optional table called REVCHANGES, which shall store entity names of modified persistent objects. Single record encapsulates the revision identifier (foreign key to REVINFO table) and a string value [1].
The mechanism of tracking modified entity names can be enabled in three different ways [1]:
- set org.hibernate.envers.track_entities_changed_in_revision parameter to possitive.
- extend DefaultTrackingModifiedEntitiesRevisionEntity (instead of standard DefaultRevisionEntity) with your custom revision entity.
- mark an appropriate field of a custom revision entity with @ModifiedEntityNames annotation. The property is required to be of Set<String> type.
In this blog entry, I am going to describe only the first possibility. Examples of remaining – DefaultTrackingModifiedEntitiesRevisionEntity superclass and @ModifiedEntityNames annotation – can be found in the Envers documentation and test cases (here and here).
For tutorial purpose I will use Envers demo which source code is freely available on the Hiernate Git repository (https://github.com/hibernate/hibernate-core/tree/master/hibernate-envers/src/demo). After enabling org.hibernate.envers.track_entities_changed_in_revision parameter in persistence.xml file and running TestConsole main class, REVCHANGES table is created and populated with the following content:
REV ENTITYNAME ---------------------------------------- 54 org.hibernate.envers.demo.Person 54 org.hibernate.envers.demo.Address
Enabling default mechanism of tracking modified entity names allows user to utilize the following methods exposed by CrossTypeRevisionChangesReader (available from AuditReader) interface [1]:
- Set<Pair<String, Class>> findEntityTypes(Number revision) – Returns set of entity names and corresponding Java classes modified in a given revision.
- List<Object> findEntities(Number revision) – Find all entities changed (added, updated and removed) in a specified revision.
- List<Object> findEntities(Number revision, RevisionType revisionType) – Returns snapshots of all audited entities changed (added, updated or removed) in a given revision filtered by modification type.
- Map<RevisionType, List<Object>> findEntitiesGroupByRevisionType(Number revision) – Returns a map containing lists of entity snapshots grouped by modification operation (e.g. addition, update and removal).
Please note that some of operations listed above might be expensive in terms of performance. Envers has to execute several queries, which number depends on how many different entity types have changed in a specified revision. JavaDoc and documentation contains detailed runtime complexity description. For example, when retrieving all objects changed during revision 54 (List<Object> modified = getAuditReader().getCrossTypeRevisionChangesReader().findEntities(54);), the following three queries are executed:
Hibernate: select this_.REV as REV4_0_, this_.REVTSTMP as REVTSTMP4_0_, modifieden2_.REV as REV4_2_, modifieden2_.ENTITYNAME as ENTITYNAME2_ from REVINFO this_ left outer join REVCHANGES modifieden2_ on this_.REV=modifieden2_.REV where this_.REV in ( ? ) Hibernate: select address_au0_.id as id2_, address_au0_.REV as REV2_, address_au0_.REVTYPE as REVTYPE2_, address_au0_.flatNumber as flatNumber2_, address_au0_.houseNumber as houseNum5_2_, address_au0_.streetName as streetName2_ from Address_AUD address_au0_ where address_au0_.REV=? Hibernate: select person_aud0_.id as id3_, person_aud0_.REV as REV3_, person_aud0_.REVTYPE as REVTYPE3_, person_aud0_.name as name3_, person_aud0_.surname as surname3_, person_aud0_.address_id as address6_3_ from Person_AUD person_aud0_ where person_aud0_.REV=?
The first one returns all entity types modified in a given revision. Remaining SQL statements retrieve essential data from proper audit tables.
From my previous experience with various frameworks, I’ve realized that most of the time the default behavior is not enough. Users, that wish to customize tracking of modified entity types, might want to implement EntityTrackingRevisionListener interface. In this case they shall not enable the default mechanism (by for example leaving negative value of org.hibernate.envers.track_entities_changed_in_revision parameter). EntityTrackingRevisionListener exposes one method that notifies whenever audited entity instance has been added, modified or removed within current revision boundaries [1]. entityChanged() procedure provides user with information about modified entity class and name, its identifier, revision type and revision entity. BTW Community – do you think that active Hibernate session would be usefull as well?
Example implementation of EntityTrackingRevisionListener interface [1]:
public class CustomEntityTrackingRevisionListener implements EntityTrackingRevisionListener { @Override public void entityChanged(Class entityClass, String entityName, Serializable entityId, RevisionType revisionType, Object revisionEntity) { String type = entityClass.getName(); ((CustomTrackingRevisionEntity)revisionEntity).addModifiedEntityType(type); } @Override public void newRevision(Object revisionEntity) { } }
CustomEntityTrackingRevisionListener shall be passed as the value of @RevisionEntity annotation [1]:
@Entity @RevisionEntity(CustomEntityTrackingRevisionListener.class) public class CustomTrackingRevisionEntity { @Id @GeneratedValue @RevisionNumber private int customId; @RevisionTimestamp private long customTimestamp; @OneToMany(mappedBy="revision", cascade={CascadeType.PERSIST, CascadeType.REMOVE}) private Set<ModifiedEntityTypeEntity> modifiedEntityTypes = new HashSet<ModifiedEntityTypeEntity>(); public void addModifiedEntityType(String entityClassName) { modifiedEntityTypes.add(new ModifiedEntityTypeEntity(this, entityClassName)); } ... } @Entity public class ModifiedEntityTypeEntity { @Id @GeneratedValue private Integer id; @ManyToOne private CustomTrackingRevisionEntity revision; private String entityClassName; ... }
To retrieve entity class names modified in a given revision, you have to query for corresponding revision entity and then access modified entity types.
CustomTrackingRevisionEntity ctre = getAuditReader().findRevision(CustomTrackingRevisionEntity.class, 54); Set<ModifiedEntityTypeEntity> modifiedEntityTypes = ctre.getModifiedEntityTypes();
Please remember to use cascade option in the mapping of @RevisionEntity‘s relations (see CustomTrackingRevisionEntity.modifiedEntityTypes). While persisting each revision entity (master), Envers will not automatically cascade any operation to related objects (details).
In case of implementing EntityTrackingRevisionListener, the execution of AuditReader.getCrossTypeRevisionChangesReader() method will raise an exception. Users have to design and code their own API that retrieves snapshots of entities modified during specified revision.
That’s all folks :). Feel free to comment and post your suggestions.
References:
[1] Hibernate Envers documentation http://docs.jboss.org/hibernate/core/4.0/devguide/en-US/html/ch15.html.
Pingback: Envers: track entity types changed in revisions @ Blog of Adam Warski
Any thoughts on how this functionality can be either back-ported or simulated in the current 3.x stream (ideally without modifying envers code)? I’d like this feature but can’t move to hibernate 4 yet.
I will consult applying this feature to 3.x stream with Adam Warski and let you know ASAP. This cannot be achieved without modifying Envers source code. BTW sorry for such a delay in posting an answer – summer holidays.
Today in the morning one brutal solution came to my mind :). Try the following:
1. Proxy AuditProcess.executeInSession() method (for example with AOP). Before the original call, backup all elements of workUnits queue (you might need to use Java Reflection API) to another collection.
2. Implement something inspired by EntityChangeNotifier.entityChanged() and manually populate your revision entity with modified entity types. The method AuditWorkUnit.getRevisionType() may not exist, so calculate revision type basing on actual AuditWorkUnit subclass. See the constructors of: AddWorkUnit, ModWorkUnit, CollectionChangeWorkUnit, DelWorkUnit, FakeBidirectionalRelationWorkUnit (here it can be problematic to figure out the revision type) and PersistentCollectionChangeWorkUnit in the master branch. Method PersistentCollectionChangeWorkUnit.PersistentCollectionChangeWorkUnitId.getOwnerId() may not be public in your release, so use Java Reflection API.
See my changes in the following classes for reference:
– https://fisheye2.atlassian.com/browse/Hibernate-Core/hibernate-envers/src/main/java/org/hibernate/envers/synchronization/AuditProcess.java?r=c2e53061f2817ca091a5384a173b0e07067a5d2a
– https://fisheye2.atlassian.com/browse/Hibernate-Core/hibernate-envers/src/main/java/org/hibernate/envers/synchronization/EntityChangeNotifier.java?r=13c9fd4f9d177fb7d022c72d674f1a23b909c443
Is this solution acceptable for you?
Adam agreed to apply this feature to 3.x stream. This will take me some time. Which solution would be better for you? Have you tried the one proposed above?
Hy!
When will this feature be applied to the 3.x stream?
Thanks for your great work!
Hi! I think it may take me a while, e.g. two weeks. Is it acceptable for you?
Yes! Thank you!
OK. You can trace the status of submitted pull request here: https://github.com/hibernate/hibernate-core/pull/163. I have back-ported only the core functionality, but it should be enough for you to retrieve changes made during specified revision. Just create custom revision entity and revision listener.
See:
– https://github.com/hibernate/hibernate-core/blob/master/hibernate-envers/src/main/java/org/hibernate/envers/DefaultTrackingModifiedEntitiesRevisionEntity.java
If you face any issues, please let me know.