11.5. Versioning Tutorial

For a high-level overview and the basic definition of versioning, please refer to Section 4.11 Versioning Service.

Conceptually, versioning can be divided into two parts:

  1. One part concerns PDL syntax that allows the developer to declaratively request certain object types to be versioned.

  2. The other part consists of the Java API provided by the com.arsdigita.versioning package. This API provides classes and methods for interacting with and making use of the versioning service.

This tutorial explains the PDL syntax first, with examples covered in Section 11.5.5 PDL Syntax. If you wish to skip ahead, you can go directly to Section 11.5.4 Versioning service API.

The implementation and semantics of the versioning service have changed in a number of major ways after the 5.2 release. If you are familiar with the older implementation and would like a brief summary of the major differences between the old and current implementations, please refer to Section 11.5.6 Differences between WAF 5.2 and 6.0.

11.5.1. Data-Object-Level Service

The versioning service operates on the data object level. Putting the versioned keyword in front of the object type definition makes all instances of this type versioned. For example:

versioned object type Quux {
    BigInteger[1..1] id = quuces.id INTEGER;
    String[1..1] name   = quuces.name VARCHAR;
    object key(id);
}

Example 11-7. The versioned keyword

The consequences of marking an object type as versioned are explained in Section 11.5.2 Fully versioned types.

11.5.2. Fully versioned types

All instances of the object type Quux defined in Example 11-7 are versioned.

Marking one object type as versioned may impose constraints on other object types, forcing them to be versioned, too. Two most straightforward examples of this are subtyping and composite-component relationship.

object type GreatQuux extends Quux {
    String[0..1] email  = great_quuces.email VARCHAR;

    component Foobar[0..n] foobars = join great_quuces.id
                                       to foobars.great_quux_id;

    Country[1..1] country = join great_quuces.country_id
                              to countries.id;

    reference key (great_quuces.id);
}

Example 11-8. Components and required compound attributes

In Example 11-8, the GreatQuux object type is versioned, because it extends a versioned type. If the object type X is marked as versioned, then the versioning service infers that the subtypes and components of X are also versioned.

This happens because a data object fully owns its components. As part of properly versioning a data object, we must version its components. In Example 11-8, this semantic constraint dictates that the object type Foobar is also versioned.

Generally speaking, versioning a data object means recording its creation and deletion events, as well as all changes to its attributes. For example, if a Great Quux's email was [email protected] on 19 Jan. and changed to [email protected] on 12 Apr., the versioning service will have a record of this.

In the event that this data object is rolled back to its 19 Jan. state, then the email address has to be changed back to [email protected]. This is an example of versioning a scalar attribute.

We use the term scalar attribute to refer to a property that has a simple object type, such as BigInteger, Boolean, or String.

If the properties type of an object type are compound, they are referred to as compound attributes. In Example 11-8, Country is a compound object type (the exact PDL definition is not shown for the sake of brevity). Thus, the property country of the object type GreatQuux is a compound attribute.

As the example of the email property demonstrates, the case of versioning scalar attributes is a straightforward one, while compound attributes require a little extra care. Section 11.5.3 Recoverable types explains another situation in which compound attributes require special treatment.

11.5.3. Recoverable types

The case of compound attributes like country is a little more complex. Let's look at Example 11-8 again. Suppose the Great Quux had Wonderland listed as his country of residence on Jan 19. On Apr 12, his country of residence changed to Eriador; the Wonderland data object was deleted altogether. What should happen if we try to rollback the Great Quux to his Jan 19 state? Since the data object is versioned, its country attribute should be rolled back to Wonderland. But Wonderland no longer exits in the database. One solution is to set the country to null. This would not work in this case, because this attribute is required - it has the multiplicity of 1..1. Therefore, we should be able to recover the deleted data object.

This leads to the notion of a recoverable data object. A data object is recoverable if the versioning service is able to undelete it after it has been deleted. Being recoverable is a weaker requirement than being fully versioned. The versioning service may not be able to rollback a recoverable object back to an arbitrary point in its history, but it should record enough information to undelete it, in case it is ever deleted.

The versioning service infers that if a versioned object type has a required compound attribute, then the object type of the attribute must be marked as recoverable. The versioning service does not normally record changes to recoverable objects, but it keeps track of them in case they are deleted. If a recoverable object is deleted, the versioning service records the last known values of its attributes in order to be able to undelete it.

What does this mean in terms of Example 11-8? The versioning service treats the Country object type as recoverable, because it is a required compound attribute of the versioned type GreatQuux.

Why not make the Country object type fully versioned? Because we don't want to version more data than is actually needed. If a developer wants to make the Country object type fully versioned, they should explicitly mark it as such in the PDL definition.

What if the Country object type is marked as versioned? Should we rollback the country attribute when we rollback the Great Quux object? We cannot. Since it is not a component of GreatQuux, Country data objects may be included as an attribute by other object types. If we rollback a country as part of rolling back the Great Quux object, we may affect a lot of other objects that should not have been necessarily affected by this rollback.

11.5.4. Versioning service API

For backwards compatibility, some of the old versioning APIs are preserved. For example, you can tag the state of a data object in the current transaction by calling the applyTag method on the VersionedACSObject class. However, this class has been deprecated.

The preferred point of entry into the API provided by the versioning service is the Versions class. One important capability it has is providing methods for invoking the following functionalities:

  1. Tagging the state of a data object in the current transaction. Note that you can tag the state of a data object, even if no changes were made to it in the current transaction. You can also tag more than one data object within the same transaction.

  2. You can retrieve a collection of tagged transactions for a data object. Note that since the data object may have been deleted, the method for retrieving the collection takes in an OID parameter rather than a DataObject parameter.

  3. You can roll a data object back to any previous point in its history.

  4. You can compute the difference between any two states of a data object.

  5. You can suspend versioning till the end of the current transaction.

Please refer to the Javadoc API at http://rhea.redhat.com/documentation/api/ccm-core-6.1.0 for details.

11.5.5. PDL Syntax

To specify which object types should be versioned, developers can use the keywords versioned and unversioned. Consider the following example.

versioned object type VT1 {
    BigInteger[1..1] id = t_vt1.id INTEGER;
    object key(id);
}

object type VT2 extends VT1 {
    component UT3[0..n] ut3s = join t_vt2.id to t_ut3.vt2_id;
    reference key(t_vt2.id);
}

object type C1 {
    BigInteger[1..1] id = t_c1.id INTEGER;
    A1[1..1] a1s = join t_c1.a1_id to t_a1.id;
    component UT4[0..1] ut4 = join t_c1.ut4_id to t_ut4.id;
    object key(id);
}

association {
    VT2[0..1] vt2 = join t_c1.vt2_id to t_vt2.id;
    component C1[0..n] c1s = join t_vt2.id to t_c1.vt2_id;
}

object type A1 {
    BigInteger [1..1] id = t_a1.id INTEGER;
    UT4[1..1] ut4 = join t_a1.ut4_id to t_ut4.id;
    object key(id);
}

object type UT1 {
    BigInteger[1..1] id = t_ut1.id INTEGER;
    UT3[1..1] ut3attr = join t_ut1.ut3_id to t_ut3.id;
    object key(id);
}

object type UT2 {
    BigInteger[1..1] id = t_ut2.id INTEGER;
    object key(id);
}

versioned object type VUT2 extends UT2 {
  UT1[1..1] ut1 = join t_vut2.ut1id to t_ut1.id;
  unversioned String[1..1] unverAttr = t_vut2.unver_attr VARCHAR;
  unversioned component UT5[0..n] ut5 = join t_vut2.ut5id to t_ut5.id;
  versioned UT6[0..n] ut6s = join t_vut2.ut6id to t_ut6.id;
  reference key(t_vut2.id);
}

object type UT3 {
    BigInteger[1..1] id = t_ut3.id INTEGER;
    composite VT2[1..1] vt2 = join t_ut3.vt2_id to t_vt2.id;
    object key(id);
}

object type UT4 {
    BigInteger[1..1] id = t_ut4.id INTEGER;
    object key(id);
}


versioned object type VTC3 {
    BigInteger[1..1] id = t_vtc3.id INTEGER;
    object key(id);
}

object type C2 {
    BigInteger[1..1] id = t_c2.id INTEGER;
    composite VTC3[1..1] vtc3 = join t_c2.composite_id to t_vtc3.id;
    object key(id);
}

object type UT5 {
    BigInteger[1..1] id = t_ut5.id INTEGER;
    object key(id);
}

object type UT6 {
    BigInteger[1..1] id = t_ut6.id INTEGER;
    object key(id);
}

Example 11-9. Sample PDL definitions

For the sake of brevity, Example 11-9 only has key attributes for object types and does not show any scalar attributes.

The graph in Figure 11-8 provides a visual representation of the above PDL definitions. Wheat-colored nodes represent object types that are marked as versioned in Example 11-9. There are two types of edges. If an edge is labeled extends, it means that the type at the edge head extends, in the PDL sense, the type at the edge tail. For example, VT2 extends VT1. This relationship is important, because subtypes of a versioned type are also versioned.

The other kind of edge is one that shows attributes of an object type. For example, consider the edge labeled rqd:ut3attr that connects UT1 and UT3. It means that the object type UT1 has a required attribute ut3attr of type UT3.

There are two kinds of compound attributes that are important from the versioning point of view: required (rqd) and components (cnt).

Figure 11-8. PDL definition graph

Based on Figure 11-8, the versioning service decides which object types should be versioned, recoverable, or simply ignored. The result of this decision can be visualized as follows.

Figure 11-9. Versioning dependence graph

Red-colored nodes indicate those object types that are fully versioned by virtue of being marked versioned (vnd) or having a supertype that is marked vnd.

Pink-colored nodes represent those types that are co-versioned by virtue of being a component of a versioned or co-versioned object type.

Yellow-colored nodes denote those types that are treated as recoverable by virtue of being a required attribute of a versioned or recoverable type.

Gray nodes represent types that are ignored by the versioning service.

There are two kinds of edges in Figure 11-9:

  1. Unlabeled edges represent the subtype relationship between two nodes. For example, VUT2 extends UT2.

  2. Labeled edges are those where the edge tail is an object type that has an attribute whose type is the edge head. For example, VT2 has the component attribute c1s of type C1.

Four kinds of attributes are important from the versioning perspective: marked versioned (vnd), marked unversioned (unv), required (rqd) and components (cnt).

11.5.6. Differences between WAF 5.2 and 6.0

If you used the versioning service in previous releases of WAF prior to and including 5.2, you will notice a number of differences between the old and current implementation.

The old implementation operated on the domain object level. The current implementation operates on the data object level. Key differences that stem from this are:

  1. Previously, you had to subclass VersionedACSObject in order to make your domain objects versioned. This is no longer necessary.

  2. Consequently, you are no longer required to use the methods getMaster(), setMaster(), etc. The notion of master is gone. Although marked deprecated, these methods remain in order to provide some degree of backwards compatibility on the API level.

  3. In the old versioning systems, calling the delete() method never actually deleted the underlying data object. It merely marked it as deleted via the is_deleted flag in the vc_objects table. This was referred to as a soft delete. It was possible to hard-delete an object via the permanentlyDelete() method of the VersionedACSObject class. This is no longer supported. In the current versioning system, all deletes are hard deletes.

    As a consequence, it is no longer necessary to write queries such as this:

    select
      f.id, frob_name
    from
      frobs f,
      vc_objects v
    where
      f.id = v.object_id
      and v.is_deleted = '0';

    Instead, one can simply write:

    select
      f.id, frob_name
    from
      frobs f
  4. The old versioning provided the ability to turn off versioning on a per instance basis. The method trackChanges() could be overloaded to tell the versioning service whether or not this domain object wished to be versioned. The new versioning service no longer provides this capability. What it offers instead is the ability to turn off versioning entirely until the end of the current transaction by calling Versions.suspendVersioning().

    It is possible to request that an attribute of an object be not versioned. For instance:

    versioned object type Frob {
      BigInteger[1..1] id = frobs.id INTEGER;
    
      unversioned String[0..1] type   = frobs.type VARCHAR;
      String[1..1] name   = frobs.frob_name VARCHARA;
    }

    In the above example, the versioning system will ignore changes to the type attribute of the Frob object type.

  5. The current versioning service provides the ability to compute a diff between any two points in history of a versioned data object. The old service did not provide a similar capability.