Hibernate Envers – Tracking entity names modified during revisions (HHH-5580)

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.

HHH-4073 and so you code…

Dłuższy czas nie pisałem nic na blogu. Na fakt tez złożyło się szereg okoliczności – GeeCON, nawałnica w pracy związana z produkcyjnym uruchomieniem trzyletniego projektu, oraz nowe odcinki seriali Californication i Stargate Universe. Główną przyczyną moich zaniedbań stało się jednak aktywne uczestnictwo w open source’owym projekcie Hibernate Envers (moduł wchodzący w skład Hibernate Core).

Zaczęło się od krótkiego tutorialu dotyczącego serwera kontroli wersji Git, fork’u repozytorium Hibernate oraz ciężkich starć z Gradle. Po około tygodniu zmagań z przygotowaniem środowiska wrzuciłem pierwszą zakończoną sukcesem poprawkę HHH-4073. Nie czekając na oklaski rozwiązywałem dalej HHH-4787, HHH-5276, HHH-5808 oraz HHH-6069. Doszedłem w końcu do pierwszego poważnego pull request’u, w którym to zaimplementowałem (z pomocą team leader’a Adama Warskiego) mechanizm śledzenia zmodyfikowanych, w ramach kolejnych rewizji, encji – https://github.com/hibernate/hibernate-core/pull/84.

Co będzie dalej zobaczymy… Muszę przyznać, że zastanawianie się dobre kilkanaście minut nad pojedynczą linijką kodu oraz wymiana doświadczeń z innymi, bywają bardzo pouczające.

Zdjęcie z GeeCON 2011 (University Day):

Łukasz Antoniak, GeeCON 2011