auditing of Hibernate mappings, which extend JPA, like custom types and collections/maps of "simple" types (Strings, Integers, etc.) (see also Chapter 9, Mapping exceptions)
logging data for each revision using a "revision entity"
querying historical data
<persistence-unit ...> <provider>org.hibernate.ejb.HibernatePersistence</provider> <class>...</class> <properties> <property name="hibernate.dialect" ... /> <!-- other hibernate properties --> <property name="hibernate.ejb.event.post-insert" value="org.hibernate.ejb.event.EJB3PostInsertEventListener,org.hibernate.envers.event.AuditEventListener" /> <property name="hibernate.ejb.event.post-update" value="org.hibernate.ejb.event.EJB3PostUpdateEventListener,org.hibernate.envers.event.AuditEventListener" /> <property name="hibernate.ejb.event.post-delete" value="org.hibernate.ejb.event.EJB3PostDeleteEventListener,org.hibernate.envers.event.AuditEventListener" /> <property name="hibernate.ejb.event.pre-collection-update" value="org.hibernate.envers.event.AuditEventListener" /> <property name="hibernate.ejb.event.pre-collection-remove" value="org.hibernate.envers.event.AuditEventListener" /> <property name="hibernate.ejb.event.post-collection-recreate" value="org.hibernate.envers.event.AuditEventListener" /> </properties> </persistence-unit>
import org.hibernate.envers.Audited; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.GeneratedValue; import javax.persistence.Column; @Entity @Audited // that's the important part :) public class Person { @Id @GeneratedValue private int id; private String name; private String surname; @ManyToOne private Address address; // add getters, setters, constructors, equals and hashCode here }
@Entity @Audited public class Address { @Id @GeneratedValue private int id; private String streetName; private Integer houseNumber; private Integer flatNumber; @OneToMany(mappedBy = "address") private Set<Person> persons; // add getters, setters, constructors, equals and hashCode here }
AuditReader reader = AuditReaderFactory.get(entityManager); Person oldPerson = reader.find(Person.class, personId, revision)
entityManager.getTransaction().begin(); Address address1 = new Address("Privet Drive", 4); Person person1 = new Person("Harry", "Potter", address1); Address address2 = new Address("Grimmauld Place", 12); Person person2 = new Person("Hermione", "Granger", address2); entityManager.persist(address1); entityManager.persist(address2); entityManager.persist(person1); entityManager.persist(person2); entityManager.getTransaction().commit();
entityManager.getTransaction().begin(); Address address1 = entityManager.find(Address.class, address1.getId()); Person person2 = entityManager.find(Person.class, person2.getId()); // Changing the address's house number address1.setHouseNumber(5) // And moving Hermione to Harry person2.setAddress(address1); entityManager.getTransaction().commit();
We can retrieve the old versions (the audit) easily:
AuditReader reader = AuditReaderFactory.get(entityManager); Person person2_rev1 = reader.find(Person.class, person2.getId(), 1); assert person2_rev1.getAddress().equals(new Address("Grimmauld Place", 12)); Address address1_rev1 = reader.find(Address.class, address1.getId(), 1); assert address1_rev1.getPersons().getSize() == 1; // and so on
To start working with Envers, all configuration that you must do is add the event listeners to persistence.xml, as described in the Chapter 1, Quickstart.
However, as Envers generates some entities, and maps them to tables, it is possible to set the prefix and suffix that is added to the entity name to create an audit table for an entity, as well as set the names of the fields that are generated.
In more detail, here are the properites that you can set:
To change the name of the revision table and its fields (the table, in which the
numbers of revisions and their timestamps are stored), you can use the
@RevisionEntity
annotation.
For more information, see Chapter 4, Logging data for revisions.
To set the value of any of the properties described above, simply add an entry to
your persistence.xml
. For example:
<persistence-unit ...> <provider>org.hibernate.ejb.HibernatePersistence</provider> <class>...</class> <properties> <property name="hibernate.dialect" ... /> <!-- other hibernate properties --> <property name="hibernate.ejb.event.post-insert" value="org.hibernate.ejb.event.EJB3PostInsertEventListener,org.hibernate.envers.event.AuditEventListener" /> <property name="hibernate.ejb.event.post-update" value="org.hibernate.ejb.event.EJB3PostUpdateEventListener,org.hibernate.envers.event.AuditEventListener" /> <property name="hibernate.ejb.event.post-delete" value="org.hibernate.ejb.event.EJB3PostDeleteEventListener,org.hibernate.envers.event.AuditEventListener" /> <property name="hibernate.ejb.event.pre-collection-update" value="org.hibernate.envers.event.AuditEventListener" /> <property name="hibernate.ejb.event.pre-collection-remove" value="org.hibernate.envers.event.AuditEventListener" /> <property name="hibernate.ejb.event.post-collection-recreate" value="org.hibernate.envers.event.AuditEventListener" /> <property name="org.hibernate.envers.versionsTableSuffix" value="_V" /> <property name="org.hibernate.envers.revisionFieldName" value="ver_rev" /> <!-- other envers properties --> </properties> </persistence-unit>
The EJB3Post...EvenListener
s are needed, so that ejb3 entity lifecycle callback
methods work (@PostPersist, @PostUpdate, @PostRemove
.
You can also set the name of the audit table on a per-entity basis, using the
@AuditTable
annotation. It may be tedious to add this
annotation to every audited entity, so if possible, it's better to use a prefix/suffix.
If you have a mapping with secondary tables, audit tables for them will be generated in
the same way (by adding the prefix and suffix). If you wish to overwrite this behaviour,
you can use the @SecondaryAuditTable
and
@SecondaryAuditTables
annotations.
If you'd like to override auditing behaviour of some fields/properties in an embedded component, you can use
the @AuditOverride(s)
annotation on the place where you use the component.
If you want to audit a relation mapped with @OneToMany+@JoinColumn
,
please see Chapter 9, Mapping exceptions for a description of the additional
@AuditJoinTable
annotation that you'll probably want to use.
If you want to audit a relation, where the target entity is not audited (that is the case for example with
dictionary-like entities, which don't change and don't have to be audited), just annotate it with
@Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED)
. Then, when reading historic
versions of your entity, the relation will always point to the "current" related entity.
This entity must have at least two properties:
package org.jboss.envers.example; import org.hibernate.envers.RevisionEntity; import org.hibernate.envers.DefaultRevisionEntity; import javax.persistence.Entity; @Entity @RevisionEntity(ExampleListener.class) public class ExampleRevEntity extends DefaultRevisionEntity { private String username; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } }
Or, if you don't want to extend any class:
package org.hibernate.envers.example; import org.hibernate.envers.RevisionNumber; import org.hibernate.envers.RevisionTimestamp; import org.hibernate.envers.RevisionEntity; import javax.persistence.Id; import javax.persistence.GeneratedValue; import javax.persistence.Entity; @Entity @RevisionEntity(ExampleListener.class) public class ExampleRevEntity { @Id @GeneratedValue @RevisionNumber private int id; @RevisionTimestamp private long timestamp; private String username; // Getters, setters, equals, hashCode ... }
package org.hibernate.envers.example; import org.hibernate.envers.RevisionListener; import org.jboss.seam.security.Identity; import org.jboss.seam.Component; public class ExampleListener implements RevisionListener { public void newRevision(Object revisionEntity) { ExampleRevEntity exampleRevEntity = (ExampleRevEntity) revisionEntity; Identity identity = (Identity) Component.getInstance("org.jboss.seam.security.identity"); exampleRevEntity.setUsername(identity.getUsername()); } }
You can think of historic data as having two dimension. The first - horizontal - is the state of the database at a given revision. Thus, you can query for entities as they were at revision N. The second - vertical - are the revisions, at which entities changed. Hence, you can query for revisions, in which a given entity changed.
The queries in Envers are similar to Hibernate Criteria, so if you are common with them, using Envers queries will be much easier.
The main limitation of the current queries implementation is that you cannot traverse relations. You can only specify constraints on the ids of the related entities, and only on the "owning" side of the relation. This however will be changed in future releases.
Please note, that queries on the audited data will be in many cases much slower than corresponding queries on "live" data, as they involve correlated subselects.
The entry point for this type of queries is:
AuditQuery query = getAuditReader().createQuery().forEntitiesAtRevision(MyEntity.class, revisionNumber);
query.add(AuditEntity.property("name").eq("John"));
And to select only entites that are related to a given entity:
query.add(AuditEntity.property("address").eq(relatedEntityInstance)); // or query.add(AuditEntity.relatedId("address").eq(relatedEntityId));
A full query, can look for example like this:
List personsAtAddress = getAuditReader().createQuery() .forEntitiesAtRevision(Person.class, 12) .addOrder(AuditEntity.property("surname").desc()) .add(AuditEntity.relatedId("address").eq(addressId)) .setFirstResult(4) .setMaxResults(2) .getResultList();
The entry point for this type of queries is:
AuditQuery query = getAuditReader().createQuery() .forRevisionsOfEntity(MyEntity.class, false, true);
AuditEntity.revisionNumber()
you can specify constraints, projections
and order on the revision number, in which the audited entity was modified
AuditEntity.revisionProperty(propertyName)
you can specify constraints,
projections and order on a property of the revision entity, corresponding to the revision
in which the audited entity was modified
AuditEntity.revisionType()
gives you access as above to the type of
the revision (ADD, MOD, DEL).
Number revision = (Number) getAuditReader().createQuery() .forRevisionsOfEntity(MyEntity.class, false, true) .setProjection(AuditEntity.revisionNumber().min()) .add(AuditEntity.id().eq(entityId)) .add(AuditEntity.revisionNumber().gt(42)) .getSingleResult();
Number revision = (Number) getAuditReader().createQuery() .forRevisionsOfEntity(MyEntity.class, false, true) // We are only interested in the first revision .setProjection(AuditEntity.revisionNumber().min()) .add(AuditEntity.property("actualDate").minimize() .add(AuditEntity.property("actualDate").ge(givenDate)) .add(AuditEntity.id().eq(givenEntityId))) .getSingleResult();
<target name="schemaexport" depends="build-demo" description="Exports a generated schema to DB and file"> <taskdef name="hibernatetool" classname="org.hibernate.tool.ant.EnversHibernateToolTask" classpathref="build.demo.classpath"/> <hibernatetool destdir="."> <classpath> <fileset refid="lib.hibernate" /> <path location="${build.demo.dir}" /> <path location="${build.main.dir}" /> </classpath> <jpaconfiguration persistenceunit="ConsolePU" /> <hbm2ddl drop="false" create="true" export="false" outputfilename="versioning-ddl.sql" delimiter=";" format="true"/> </hibernatetool> </target>
Will generate the following schema:
create table Address ( id integer generated by default as identity (start with 1), flatNumber integer, houseNumber integer, streetName varchar(255), primary key (id) ); create table Address_AUD ( id integer not null, REV integer not null, flatNumber integer, houseNumber integer, streetName varchar(255), REVTYPE tinyint, primary key (id, REV) ); create table Person ( id integer generated by default as identity (start with 1), name varchar(255), surname varchar(255), address_id integer, primary key (id) ); create table Person_AUD ( id integer not null, REV integer not null, name varchar(255), surname varchar(255), REVTYPE tinyint, address_id integer, primary key (id, REV) ); create table REVINFO ( REV integer generated by default as identity (start with 1), REVTSTMP bigint, primary key (REV) ); alter table Person add constraint FK8E488775E4C3EA63 foreign key (address_id) references Address;
For each audited entity (that is, for each entity containing at least one audited field), an audit
table is created. By default, the audit table's name is created by adding a "_AUD" suffix to
the original name, but this can be overriden by specifing a different suffix/prefix
(see Chapter 3, Configuration) or on a per-entity basis using the
@AuditTable
annotation.
The audit table has the following fields:
id of the original entity (this can be more then one column, if using an embedded or multiple id)
revision number - an integer
revision type - a small integer
audited fields from the original entity
The primary key of the audit table is the combination of the original id of the entity and the revision number - there can be at most one historic entry for a given entity instance at a given revision.
The current entity data is stored in the original table and in the audit table. This is a duplication of data, however as this solution makes the query system much more powerful, and as memory is cheap, hopefully this won't be a major drawback for the users. A row in the audit table with entity id ID, revision N and data D means: entity with id ID has data D from revision N upwards. Hence, if we want to find an entity at revision M, we have to search for a row in the audit table, which has the revision number smaller or equal to M, but as large as possible. If no such row is found, or a row with a "deleted" marker is found, it means that the entity didn't exist at that revision.
The "revision type" field can currently have three values: 0, 1, 2, which means, respectively, ADD, MOD and DEL. A row with a revision of type DEL will only contain the id of the entity and no data (all fields NULL), as it only serves as a marker saying "this entity was deleted at that revision".
Additionaly, there is a "REVINFO" table generated, which contains only two fields:
the revision id and revision timestamp. A row is inserted into this table on each
new revision, that is, on each commit of a transaction, which changes audited data.
The name of this table can be configured, as well as additional content stored,
using the @RevisionEntity
annotation, see Chapter 4, Logging data for revisions.
While global revisions are a good way to provide correct auditing of relations, some people have pointed out that this may be a bottleneck in systems, where data is very often modified. One viable solution is to introduce an option to have an entity "locally revisioned", that is revisions would be created for it independently. This wouldn't enable correct versioning of relations, but wouldn't also require the "REVINFO" table. Another possibility if to have "revisioning groups", that is groups of entities which share revision numbering. Each such group would have to consist of one or more strongly connected component of the graph induced by relations between entities. Your opinions on the subject are very welcome on the forum! :)
You can check out the source code from SVN, or browse it using FishEye.
The tests use, by default, use a H2 in-memory database. The configuration
file can be found in src/test/resources/hibernate.test.cfg.xml
.
The tests use TestNG, and can be found in the
org.hibernate.envers.test.integration
package
(or rather, in subpackages of this package).
The tests aren't unit tests, as they don't test individual classes, but the behaviour
and interaction of many classes, hence the name of package.
A test normally consists of an entity (or two entities) that will be audited and extends the
AbstractEntityTest
class, which has one abstract method:
configure(Ejb3Configuration)
. The role of this method is to add the entities
that will be used in the test to the configuration.
The test data is in most cases created in the "initData" method (which is called once before the tests from this class are executed), which normally creates a couple of revisions, by persisting and updating entities. The tests first check if the revisions, in which entities where modified are correct (the testRevisionCounts method), and if the historic data is correct (the testHistoryOfXxx methods).
One special case are relations mapped with @OneToMany
+@JoinColumn
on
the one side, and @ManyToOne
+@JoinColumn(insertable=false, updatable=false
)
on the many side.
Such relations are in fact bidirectional, but the owning side is the collection (see alse
here).
To properly audit such relations with Envers, you can use the @AuditMappedBy
annotation.
It enables you to specify the reverse property (using the mappedBy
element). In case
of indexed collections, the index column must also be mapped in the referenced entity (using
@Column(insertable=false, updatable=false)
, and specified using
positionMappedBy
. This annotation will affect only the way
Envers works. Please note that the annotation is experimental and may change in the future.
With the inclusion of Envers as a Hibernate module, some of the public API and configuration defaults
changed. In general, "versioning" is renamed to "auditing" (to avoid confusion with the annotation used
for indicating an optimistic locking field - @Version
).
Because of changing some configuration defaults, there should be no more problems using Envers out-of-the-box with Oracle and other databases, which don't allow tables and field names to start with "_".
<persistence-unit ...> <provider>org.hibernate.ejb.HibernatePersistence</provider> <class>...</class> <properties> <property name="hibernate.dialect" ... /> <!-- other hibernate properties --> <!-- Envers listeners --> <property name="org.hibernate.envers.auditTableSuffix" value="_versions" /> <property name="org.hibernate.envers.revisionFieldName" value="_revision" /> <property name="org.hibernate.envers.revisionTypeFieldName" value="_rev_type" /> <!-- other envers properties --> </properties> </persistence-unit>
See Chapter 3, Configuration for details on the configuration and a description of the configuration options.
JIRA issue tracker (when adding issues concerning Envers, be sure to select the "envers" component!)
Copyright © 2004 Red Hat Middleware, LLC.