Versioning

Table of Contents

A significant use-case that the CDM aims to support is that of web-based or networked nomenclators, taxonomic treatments, and other applications that serve authoritative, dynamic data for (re-)use by taxonomists and other software applications. As an example, a CDM store containing a web-based monograph or revision of a particular plant or animal family might be referenced by other taxonomists, or other taxonomic databases that deal with the same taxa. To allow applications to record and resolve changes to data over time, for example, to allow users or client applications to determine how a taxonomic classification or species page has been altered since they last accessed that information, the CDM has a fine-grained versioning functionality that records changes to objects and their relationships, and allows the prior state of the dataset to be reconstructed.

The CDM uses hibernate-envers, a versioning / auditing library that is part of the hibernate core library. The versioning functionality is limited by the features that envers provides. Envers stores changes to entities on a per-transaction basis. Consequently, it is not possible to resolve changes that take place within the same transaction. Each transaction results in the creation of an AuditEvent object that provides metadata about the audit event and also allows the state of the database at that point to be reconstructed (because an AuditEvent represents a point in time across the entire database, rather than on a per-object basis). To learn more about envers and the way that it versions data, check out the presentation given by its creator, Adam Warski here.

Versioning is enabled by default, and calls to methods like save, update, and delete, will automatically result in data being versioned. Application developers only need to be aware of the existence of versioning when reading data, and only then if they wish to retrieve an object in its prior state. If applications wish to retrieve objects from the current state of the database, they do not need to perform any additional operations.

Because versions of objects are related to a global AuditEvent, and because applications may call several service layer methods when retrieving data for presentation in a particular view, the CDM stores the AuditEvent in the static field of an object called AuditEventContextHolder, allowing the CDM and any application code to discover which particular AuditEvent a view relates to without needing to pass the AuditEvent explicitly as a method parameter (this pattern is borrows from the SecurityContext class in Spring-Security).

To query the CDM at a particular AuditEvent, applications need to place the AuditEvent in to the AuditEventContextHolder and then call DAO methods as usual.

// This would retrieve the current version of the taxon with a matching uuid.
Taxon taxon = taxonDao.find(uuid);

// Set the audit event you're interested in
AuditEventContextHolder.getContext().setAuditEvent(auditEvent);

// This method call now retrieves the taxon with a matching uuid at the audit event in context
// or null if the taxon did not exist at that point.
Taxon taxon = taxonDao.find(uuid);

// Now clear the context
AuditEventContextHolder.clearContext();

// Further calls to the persistence layer will return the most recent objects

Not all DAO methods are available in non-current contexts, either because they require certain methods that Envers doesn't currently support (such as case-insensitive string comparison), or are across relationships - currently envers does not support queries that place restrictions on related entities. In some cases this will be addressed in future releases of envers, and the CDM will incorporate these new releases as they occur. Some methods rely on the free-text-search functionality provided by hibernate search. Because hibernate search (and apache Lucene) are based on an optimized set of index files that reflect the current state of the database, it is not possible to search these indices at prior events. It is unlikely that the free-text-search functionality will ever be available in non-current contexts. If an application calls such a method in a non-current context, an OperationNotSupportedInPriorViewException is thrown, giving applications an operation to recover.

Objects retrieved in prior contexts can be initialized using the propertyPaths parameter, or (if the transaction is still open) by calling accessor methods in domain objects directly (just as you would with normal hibernate-managed entities).

In addition to being able to retrieve objects at a given state, the DAOs implement the IVersionableDao interface that offers five specific methods for working with versioned objects.

Table 5.1. IVersionableDao methods

MethodDescription
List<AuditEventRecord<T>> getAuditEvents(t,
                                         Integer limit,
                                         Integer start,
                                         AuditEventSort sort,
                                         List<String> propertyPaths);

Returns a list of audit events (in order) which affected the state of an entity t. The events returned either start at the AuditEvent in context and go forward in time (AuditEventSort.FORWARDS) or backwards in time (AuditEventSort.BACKWARDS). If the AuditEventContext is set to null, or to AuditEvent.CURRENT_VIEW, then all relevant AuditEvents are returned.

int countAuditEvents(t,
                     AuditEventSort sort);

Returns a count of audit events which affected the state of an entity t.

AuditEventRecord<T> getNextAuditEvent(t);

A convenience method which returns a record of the next (relative to the audit event in context).

AuditEventRecord<T> getPreviousAuditEvent(t);

A convenience method which returns a record of the previous (relative to the audit event in context).

boolean existed(UUID uuid);

Returns true if an object with uuid matching the one supplied either currently exists, or existed previously and has been deleted from the current view.