© Joseph B. Ottinger, Jeff Linwood and Dave Minter 2016

Joseph B. Ottinger, Jeff Linwood and Dave Minter, Beginning Hibernate, 10.1007/978-1-4842-2319-2_13

13. Hibernate Envers

Joseph B. Ottinger, Jeff Linwood2 and Dave Minter3

(1)Youngsville, North Carolina, USA

(2)Austin, Texas, USA

(3)London, UK

Hibernate Envers is a project that provides access to entity audit data and versioning and audit data. This means that if you’ve marked an entity as being audited - via the rather cleverly named @Audited annotation - that Hibernate will track changes made to that entity, and you can access the entity as it’s existed through time.

Making Envers Available to Your Project

It’s very easy to provide entity versioning to a Hibernate project: in Maven , you simply make sure to refer to the hibernate-envers artifact. Our pom.xml for this chapter will look like this:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>hibernate-parent</artifactId>
        <groupId>com.autumncode.books.hibernate</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>chapter13</artifactId>


    <dependencies>
        <dependency>
            <groupId>com.autumncode.books.hibernate</groupId>
            <artifactId>util</artifactId>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-c3p0</artifactId>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>com.mchange</groupId>
            <artifactId>c3p0</artifactId>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-envers</artifactId>
        </dependency>
    </dependencies>
</project>

Remember, we’re using a parent project to set the versions of our dependencies (through a dependencyManagement block). Not doing so means that we’re going to have to manually install our util module so that this module can access it.

Now let’s take a look at our entity. For this chapter, we’re going to use the User entity from Chapter 11, moved to a new package. We’re also going to modify it to serve our purposes, mostly by adding an annotation - @Audited - to every column for which we desire a history. Here’s our new User.java file:

package chapter13.model;

import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.envers.Audited;


import javax.persistence.*;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;


@Entity
@Data
@NoArgsConstructor
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    Integer id;
    @Audited
    @Column(unique = true)
    String name;
    @Audited
    boolean active;
    @Audited
    @ElementCollection
    Set<String> groups;
    @Audited
    String description;


    public User(String name, boolean active) {
        this.name = name;
        this.active = active;
    }


    public void addGroups(String... groupSet) {
        if (getGroups() == null) {
            setGroups(new HashSet<>());
        }
        getGroups().addAll(Arrays.asList(groupSet));


    }
}

Lastly, we need to configure Hibernate before we can demonstrate Envers. Here’s a hibernate.cfg.xml for this chapter:

<?xml version="1.0"?>
<!DOCTYPE hibernate-configuration PUBLIC
        "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
        "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
    <session-factory>
        <!--  Database connection settings  -->
        <property name="connection.driver_class">org.h2.Driver</property>
        <property name="connection.url">jdbc:h2:file:./db13</property>
        <property name="connection.username">sa</property>
        <property name="connection.password"/>
        <property name="dialect">org.hibernate.dialect.H2Dialect</property>
        <!-- set up c3p0 for use -->
        <property name="c3p0.max_size">10</property>
        <!--  Echo all executed SQL to stdout  -->
        <property name="show_sql">true</property>
        <property name="use_sql_comments">true</property>


        <!--  Drop and re-create the database schema on startup  -->
        <property name="hbm2ddl.auto">create-drop</property>


        <mapping class="chapter13.model.User"/>
    </session-factory>
</hibernate-configuration>

Note that we have nothing in this configuration that suggests that Envers is being used.

It’s time for us to start showing Envers in action.

Storing a User Object

Our first test doesn’t directly involve Envers at all. We’re going to create a User and save it in a Session, and make sure that we’re able to load it again and that it looks like what we expect it to look like. We’re then going to add tests to this class such that we are working with the revision history of the User.

We’re also saving off the User entity’s id field so we can reuse it later.

package chapter13;

import chapter13.model.User;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.envers.AuditReader;
import org.hibernate.envers.AuditReaderFactory;
import org.hibernate.envers.query.AuditEntity;
import org.hibernate.envers.query.AuditQuery;
import org.testng.annotations.Test;


import java.util.List;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;


public class EnversTest {
    int[] userId = {0};


    @Test
    public void createUser() {
        SessionUtil.doWithSession((session) -> {
            User user = new User("user name", true);
            user.setDescription("first description");
            user.addGroups("group1");
            session.save(user);
            userId[0] = user.getId();
        });
        SessionUtil.doWithSession((session) -> {
            User user = session.byId(User.class).load(userId[0]);
            assertTrue(user.isActive());
            assertEquals(user.getDescription(),
                    "first description");
        });
    }
}

When this code is run, there are a number of SQL statements that get executed. Most of them are fairly normal - they’re what we’ve done all along, after all. But there are also some new SQL statements that track revision data - reflecting the initial creation of the object.

Here’s the SQL generated for H2 for this method:

call next value for hibernate_sequence

insert into User (active, description, name, id) values (?, ?, ?, ?)

insert into User_groups (User_id, groups) values (?, ?)

insert into REVINFO (REV, REVTSTMP) values (null, ?)

insert into User_AUD (REVTYPE, active, description, name, id, REV) values (?, ?, ?, ?, ?, ?)

insert into User_groups_AUD (REVTYPE, REV, User_id, groups) values (?, ?, ?, ?)

select user0_.id as id1_1_0_, user0_.active as active2_1_0_, user0_.description as descript3_1_0_, user0_.name as name4_1_0_ from User user0_ where user0_.id=?

The REVINFO, User_AUD, and User_groups_AUD table are all created automatically by Envers when Hibernate determines that the User entity has been marked as being @Audited. You can control the naming of the audit tables if you like, but we aren’t going to do that here.1

Updating the User

Our next test is very similar to the first: we’re going to update our User entity, in two separate transactions, and then we’re going to make sure that our User looks like we expect it to look. We declare a dependency between test methods because we need to make sure the createUser() test runs (and passes) before our updateUser() test .

@Test(dependsOnMethods = "createUser")
public void updateUser() {
    SessionUtil.doWithSession((session) -> {
        User user = session.byId(User.class).load(userId[0]);
        user.addGroups("group2");
        user.setDescription("other description");
    });


    SessionUtil.doWithSession((session) -> {
        User user = session.byId(User.class).load(userId[0]);
        user.setActive(false);
    });


    SessionUtil.doWithSession((session) -> {
        User user = session.byId(User.class).load(userId[0]);
        assertFalse(user.isActive());
        assertEquals(user.getDescription(), "other description");
    });
}

The SQL generated by this method is very similar in concept to the SQL generated by our first test method, with the primary difference being the addition of SQL to update the audit tables:

select user0_.id as id1_1_0_, user0_.active as active2_1_0_, user0_.description as descript3_1_0_, user0_.name as name4_1_0_ from User user0_ where user0_.id=?
select groups0_.User_id as User_id1_3_0_, groups0_.groups as groups2_3_0_ from User_groups groups0_ where groups0_.User_id=?
update User set active=?, description=?, name=? where id=?
insert into User_groups (User_id, groups) values (?, ?)
insert into REVINFO (REV, REVTSTMP) values (null, ?)
insert into User_AUD (REVTYPE, active, description, name, id, REV) values (?, ?, ?, ?, ?, ?)
insert into User_groups_AUD (REVTYPE, REV, User_id, groups) values (?, ?, ?, ?)


select user0_.id as id1_1_0_, user0_.active as active2_1_0_, user0_.description as descript3_1_0_, user0_.name as name4_1_0_ from User user0_ where user0_.id=?
update User set active=?, description=?, name=? where id=?
insert into REVINFO (REV, REVTSTMP) values (null, ?)
insert into User_AUD (REVTYPE, active, description, name, id, REV) values (?, ?, ?, ?, ?, ?)


select user0_.id as id1_1_0_, user0_.active as active2_1_0_, user0_.description as descript3_1_0_, user0_.name as name4_1_0_ from User user0_ where user0_.id=?

Accessing Envers Information

It’s finally time for us to look at some of the audit data we’ve been so dutifully writing. The primary access point for audit data is the org.hibernate.envers.AuditReader class, which we acquire from org.hibernate.envers.AuditReaderFactory. An AuditReader is scoped to a Session, so the process is to start a Session and then create an AuditReader using that Session.

Once we have an AuditReader, we have access to a lot of different views of our audited entities: we can find what revisions exist for a given entity, we can retrieve those revisions, and we can see whether an entity is audited or not. We can also create a query for audit revisions, and add criteria for which revisions to retrieve (and how many, and in what order.)

It’s not quite as flexible for audit revisions as Session is for entities, but that’s to be expected; audit data is typically write - mostly, and we don’t normally do a whole lot with apart from the primary supported features that Envers provides.

So let’s dig in.

Our first Envers-aware test will check the expected revision count for our lone User entity, and then validate some data about each one. This will show us how to get the revisions as well as how to retrieve specific revisions from an AuditReader.

@Test(dependsOnMethods = "updateUser")
public void validateRevisionData() {
    SessionUtil.doWithSession((session) -> {
        AuditReader reader = AuditReaderFactory.get(session);
        List<Number> revisions = reader.getRevisions(User.class, userId[0]);
        assertEquals(revisions.size(), 3);
        assertEquals(
                reader.find(User.class, userId[0], 1).getDescription(),
                "first description");
        assertEquals(
                reader.find(User.class, userId[0], 2).getDescription(),
                "other description");
        assertFalse(
                reader.find(User.class, userId[0], 3).isActive()
        );
    });
}

The first thing this block does is construct an AuditReader with the Session reference. Then, we get the revisions for a specific entity - in this case, our User (using the User’s id that we saved off). We should have three revisions, because that’s how many we created, after all.

Lastly, we run three assertions that use what we know about each revisions to make sure that the revisions match what we expected.

When we initially created the User, we set the description to “first description,” so the first revision’s description should match that.

The second revision was created when we updated the description to “other description,” which is also tested.

Our last revision set the active flag to false, so we can use that to check our status for the third revision.

We’re not really seeing it here, because our example is rather simple, but the entity returned from AuditReader.find() is constructed with the id of the entity and only the fields marked as being audited are included. If we were to comment out @Audited on the groups reference, for example, then the entity would set groups to null. Only audit data is loaded by AuditReader.

Querying Audited Data

Our next test method will use an AuditQueryto pull the most recent revision of our User in which the active flag was set to true. The process is remarkably similar to the Criteria API; you create a query, then add restrictions and features as desired, and then run the query.

First, let’s take a look at the actual test code itself:

@Test(dependsOnMethods = "validateRevisionData")
public void findLastActiveUserRevision() {
    SessionUtil.doWithSession((session) -> {
        AuditReader reader = AuditReaderFactory.get(session);
        AuditQuery query = reader.createQuery()
                .forRevisionsOfEntity(User.class, true, true)
                .addOrder(AuditEntity.revisionNumber().desc())
                .setMaxResults(1)
                .add(AuditEntity.id().eq(userId[0]))
                .add(AuditEntity.property("active").eq(true));


        User user = (User) query.getSingleResult();
        assertEquals(user.getDescription(), "other description");
    });
}

This code creates an AuditReader first, as one would expect.

Next, it creates an AuditQuery; we add a series of criteria and other restrictions to our query. They are the following:

  • forRevisionsOfEntity(Class<T> clazz, boolean selectEntitiesOnly, boolean selecDeletedEntities), which states that we’re interested in revisions for User entities. The first argument determines what type of entity is being queried; selectEntitiesOnly is used to control whether information about the revision is to be returned (if false, only the audited entity is returned), and selectDeletedEntities will include entities that have been deleted from the database if it’s set to true. In this case, we want only the entity and we don’t mind getting audit data for a deleted object (although in our case it has not been deleted).

  • addOrder(AuditOrder order), which allows us to control the order of results. In this case, we want the most recent User updates first.

  • setMaxResults(int), which limits the number of entities being returned.

  • add(AuditCriterion criterion), used twice in our method. The first call restricts the audit query to a specific User (with a consistent user id). The second restricts the query to User objects that were set to be active.

Then the query runs as any other query would, returning a single result (since we set the maximum result count to one entity). As we also set it to return only the entities, we can simply cast the result to User2 and then test for the values we expect.

Applying Audit Data

Our last test will do the same thing as our test to find the last active User revision. Here, we’re going to show the process for effectively reverting the data.

Unfortunately (depending on your view of the task) there’s no magic revertToRevision() method . While it’s possible that one may be created in the future, it’s very difficult to accurately predict what a given project will need to do when a change is reversed. The audit data might not be complete, after all (and therefore the reverted data would be incomplete), or perhaps not all data is meant to be reverted.

The result is that reversions are performed manually; one would look up the desired revision, load it, and then load the most recent version of the data, copying the data from Envers into the current object as desired.

It’s actually more obvious (and simple) when you see it, so let’s take a look. Most of this method is copied verbatim from the findLastActiveUserRevision() test in the previous section.

@Test(dependsOnMethods = "findLastActiveUserRevision")
public void revertUserData() {
    SessionUtil.doWithSession((session) -> {
        AuditReader reader = AuditReaderFactory.get(session);
        AuditQuery query = reader.createQuery()
                .forRevisionsOfEntity(User.class, true, true)
                .addOrder(AuditEntity.revisionNumber().desc())
                .setMaxResults(1)
                .add(AuditEntity.id().eq(userId[0]))
                .add(AuditEntity.property("active").eq(true));


        User auditUser = (User) query.getSingleResult();
        assertEquals(auditUser.getDescription(), "other description");


        // now we copy the audit data into the "current user."
        User user = session.byId(User.class).load(userId[0]);
        assertFalse(user.isActive());
        user.setActive(auditUser.isActive());
        user.setDescription(auditUser.getDescription());
        user.setGroups(auditUser.getGroups());
    });


    // let's make sure the "current user" looks like what we expect
    SessionUtil.doWithSession((session) -> {
        User user = session.byId(User.class).load(userId[0]);
        assertTrue(user.isActive());
        assertEquals(user.getDescription(), "other description");
    });
}

As promised, this is very simple code; after we have the auditUser object from the AuditReader, we load the current view of the User from the database directly. We then overwrite the User’s data with the data from the audited version of the User, which will update the database when the Session ends.

Then we validate that some of the fields match what we expect, and we’re finished.

Summary

Envers can be very useful in preserving a view of data over the lifetime of an entity. It contains a fairly easy-to-use query facility that offers views of the number of revisions as well as access to each individual revision, and provides an easy way to see the entity’s history as well as a trivial way to revert data.

Hibernate is one of the most popular mechanisms in Java for providing persistence to relational systems. We’ve shown the features that will serve most applications, including basic persistence operations (creation, reads, updates, deletes), associations between object types, multiple searching mechanisms (through HQL and the Criteria API), access to NoSQL databases and full-text search facilities, and providing and using audit data.

We’ve also seen a number of "better practices"3 in use - with an emphasis on testing and build tools (through TestNG and Maven, respectively), and we’ve also seen how to use Java 8 to streamline some of our code (in particular with the use of lambdas to hide the transaction management in our later chapters).

We hope you’ve learned some fun and interesting and, above all, relevant information as you’ve read; and we also hope you’ve enjoyed reading the book as well.

Footnotes

1 If you’re desperately interested, take a look at the arguments for the @Audited annotation.

2 At the time of writing, Envers’ AuditQuery hasn’t been made typesafe as the org.hibernate.query.Query has.

3 I wanted to say “best practices” but that sounded fairly egotistical.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset