Getting Started with Envers in Grails

Envers is a natural choice for revisioning your Grails domain classes. To get it to work with Grails is not hard but reliable information about the setup is scattered. First obstacle is to use the correct dependency setting (Grails 3.1.0):

compile('org.hibernate:hibernate-envers:4.3.11.Final') {
    transitive = false
}

The dependencies somehow break things. You would get weird exceptions about transactions. So we need to disable transitive dependencies.

Domain classes are considered for auditing when they are annotated with @Audited.

@Audited
class Machine {
    String name
    Date purchaseDate
}

A class like above spawns two tables: Machine and Machine_Aud. Current data lives in the Machine table. Older revisions are stored in Machine_Aud and can be queried with Envers’ own query tools. Each time you save, modifiy or delete a machine it will be recorded into the revision table.

Now let’s assume that your domain model dictates that the purchaseDate for your machine never changes once it is set. Putting an information into the revision again and again which is never touched would be a waste. To keep from revisioning a single field annotate its getter with @NotAudited. Annotations on fields do not seem to have any effect therefore imposing a getter just to annotate.

@Audited
class Machine {
    String name
    Date purchaseDate

    @NotAudited
    Date getPurchaseDate() {
        return purchaseDate
    }
}

Machine_Aud is now recording name only and purchaseDate not anymore.

Let’s enhance our domain model and create a Part class to track parts the machine is build of:

class Part {
    String name
    Machine machine
}

This is the simplest unidirectional one-to-many relation possible between Machine and Part. Part is not audited but references a Machine. This is no problem – audited and non-audited entities do not interfere here.

We want to make the connection bidirectional and so we add hasMany to Machine. For the first iteration, we do not want Envers to audit the connection and use @NotAudited on it:

@Audited
class Machine {

    String name
    Date purchaseDate

    @NotAudited
    Date getPurchaseDate() {
        return purchaseDate
    }

    static hasMany = [parts: Part]

    @NotAudited
    Set<Part> getParts() {
        return parts
    }
}

Why is this necessary at all? Envers audits connections because they are valueable information for the state of an audited entity. If you do not create a getter and annotate it, Envers will try to audit the connection AND will assume that the target entity is also audited. Part is not marked as audited and this would end in an exception.

The current configuration means we can request the current parts of the machine. But since they are not revisioned, they always point to the current state even if you pull out revisioned machine states.

For our final iteration, we want to make the connection of the parts from machine audited but not the parts themselves.

@Audited
class Machine {

    String name
    Date purchaseDate
    List<Part> parts

    @NotAudited
    Date getPurchaseDate() {
        return purchaseDate
    }

    static hasMany = [parts: Part]

    @Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED)
    List<Part> getParts() {
        return parts
    }
}

This achives exactly what we want. The audit data for the relation is saved in the table Machine_Part_Aud. Another little gotcha here is that you should set both sides of the bidirectional connection due to some bugs creeping around with properties not being set (could be https://github.com/grails/grails-core/issues/9290).

Machine machine
Machine.withTransaction {
    machine = new Machine(name: 'My Machine', purchaseDate: new Date()).save(failOnError: true, flush: true)
}

Part.withTransaction {
    Part p = new Part(name: 'My Part')
    machine.addToParts(p)
    p.save(failOnError: true, flush: true)
}

Above code creates revisions of a machine with parts added in the last revision.

Leave a Reply

Your email address will not be published. Required fields are marked *