YouTube video

Hibernate Envers – Getting started

By Thorben Janssen

Configuration, Mapping

A lot of business applications require an audit log that documents all changes that were performed on the managed data. There are lots of different options to implement such a log. One of them is Hibernate Envers. It just takes a few annotations to document all changes in the audit tables, and Envers also provides a powerful API to extract information from your audit log.

In this first post of the series, I will show you how to add Hibernate Envers to your project, activate auditing for an entity and retrieve different information from your log.

Project Setup

It’s pretty easy to add Hibernate Envers to an existing application. You just have to add the hibernate-envers.jar file to the classpath. If you’re using maven, you can do that with the following maven dependency.

<dependency>
  <groupId>org.hibernate</groupId>
  <artifactId>hibernate-envers</artifactId>
  <version>5.2.5.Final</version>
</dependency>

The next thing you need to do is to setup the audit tables. Hibernate can do that itself, if you use the automatic schema generation feature. But I don’t recommend that approach. You can use that feature to create your database script, but you shouldn’t deploy it to production without reviewing and improving it.

So, here are the tables you need to create in your database setup or migration script:

REVINFO

This table stores the revision information. By default, Hibernate persists only the revision number as an integer and the creation timestamp as a long.

CREATE TABLE revinfo
(
rev integer NOT NULL,
revtstmp bigint,
CONSTRAINT revinfo_pkey PRIMARY KEY (rev)
)

I will show you how to add more information to this table, like the user who created the revision, in one of the following blog posts.

An audit table for each entity

You also need to create an audit table for each entity you want to audit. By default, Hibernate adds the “_AUD” suffix to the table name of the audited entity. You can define a different table name with the @AuditTable annotation or by configuring a different prefix or suffix in the configuration.

Each audit table contains the primary key of the original entity, all audited fields, the revision number and the revision type. The revision number has to match a record in the revision table and is used together with the id column to create a combined primary key. The revision type persists the type of operation that was performed on the entity in the given revision. Envers uses the integer values 0, 1 and 2 to store that an entity was added, updated or deleted.

The following code snippet shows an example of an audit table. It stores the audit information for the Author entity and tracks all changes to the firstname and lastname attributes.

CREATE TABLE author_aud
(
id bigint NOT NULL,
rev integer NOT NULL,
revtype smallint,
firstname character varying(255),
lastname character varying(255),
CONSTRAINT author_aud_pkey PRIMARY KEY (id, rev),
CONSTRAINT author_aud_revinfo FOREIGN KEY (rev)
REFERENCES revinfo (rev) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION
)

That’s all you need to do to add Hibernate Envers to your application. You can now tell Hibernate which entities you want to audit.

Audit an entity

If you want to audit all changes of an entity, you have to annotate it with @Audited. That tells Hibernate Envers to audit the values of all attributes for all create, update and delete operations. You can, of course, apply additional annotations and configuration to adapt the audit to your needs. I’ll explain that in more detail in one of the following posts.

@Entity
@Audited
public class Author implements Serializable { … }

After you annotated your entities with @Audited, Hibernate will create a new revision for each transaction and document all changes in the audit tables. The following code snippets show basic persist and update operations and the SQL statements Hibernate executes. As you can see, you don’t need to adapt your business code in any way.

EntityManager em = emf.createEntityManager();
em.getTransaction().begin();

Author a = new Author();
a.setFirstName(“Thorben”);
a.setLastName(“Janssen”);
em.persist(a);

Book b = new Book();
b.setTitle(“Hibernate Tips”);
b.getAuthors().add(a);
a.getBooks().add(b);
em.persist(b);

em.getTransaction().commit();
em.close();
10:50:30,950 DEBUG SQL:92 - 
    select
        nextval ('hibernate_sequence')
10:50:30,989 DEBUG SQL:92 - 
    select
        nextval ('hibernate_sequence')
10:50:31,013 DEBUG SQL:92 - 
    insert 
    into
        Author
        (firstName, lastName, version, id) 
    values
        (?, ?, ?, ?)
10:50:31,024 DEBUG SQL:92 - 
    insert 
    into
        Book
        (publisherid, publishingDate, title, version, id) 
    values
        (?, ?, ?, ?, ?)
10:50:31,029 DEBUG SQL:92 - 
    insert 
    into
        BookAuthor
        (bookId, authorId) 
    values
        (?, ?)
10:50:31,042 DEBUG SQL:92 - 
    select
        nextval ('hibernate_sequence')
10:50:31,046 DEBUG SQL:92 - 
    insert 
    into
        REVINFO
        (REVTSTMP, REV) 
    values
        (?, ?)
10:50:31,048 DEBUG SQL:92 - 
    insert 
    into
        Author_AUD
        (REVTYPE, firstName, lastName, id, REV) 
    values
        (?, ?, ?, ?, ?)
10:50:31,051 DEBUG SQL:92 - 
    insert 
    into
        Book_AUD
        (REVTYPE, publishingDate, title, publisherid, id, REV) 
    values
        (?, ?, ?, ?, ?, ?)
10:50:31,054 DEBUG SQL:92 - 
    insert 
    into
        BookAuthor_AUD
        (REVTYPE, REV, bookId, authorId) 
    values
        (?, ?, ?, ?)
Book b = em.find(Book.class, b.getId());
b.setTitle(“Hibernate Tips – 64 Tips for your day to day work”)
10:49:29,465 DEBUG SQL:92 - 
    select
        book0_.id as id1_2_0_,
        book0_.publisherid as publishe5_2_0_,
        book0_.publishingDate as publishi2_2_0_,
        book0_.title as title3_2_0_,
        book0_.version as version4_2_0_,
        publisher1_.id as id1_6_1_,
        publisher1_.name as name2_6_1_,
        publisher1_.version as version3_6_1_ 
    from
        Book book0_ 
    left outer join
        Publisher publisher1_ 
            on book0_.publisherid=publisher1_.id 
    where
        book0_.id=?
10:49:29,483 DEBUG SQL:92 - 
    update
        Book 
    set
        publisherid=?,
        publishingDate=?,
        title=?,
        version=? 
    where
        id=? 
        and version=?
10:49:29,487 DEBUG SQL:92 - 
    select
        nextval ('hibernate_sequence')
10:49:29,489 DEBUG SQL:92 - 
    insert 
    into
        REVINFO
        (REVTSTMP, REV) 
    values
        (?, ?)
10:49:29,491 DEBUG SQL:92 - 
    insert 
    into
        Book_AUD
        (REVTYPE, publishingDate, title, publisherid, id, REV) 
    values
        (?, ?, ?, ?, ?, ?)

Retrieve basic audit information

Hibernate Envers provides an extensive Query API which you can use to extract the required information from your audit log. In this post, I just show you how to retrieve all revisions of an entity and how to retrieve the revision that was active at a certain point in time. These are only 2 basic use cases, and you can do a lot more with the Query API. I will show you that in more detail in a future blog post.

Get all revisions of an entity

The first thing you need to do to access your audit information is to create an AuditReader via the AuditReaderFactory. You can see an example of it in the first line of the following code snippet. I call the get method of the AuditReaderFactor with the current instance of the EntityManager.

The getRevisions method of the AuditReader returns all revision numbers of a given entity. You can use these numbers to get an entity with all the attribute values it had at a given revision. I do that in the 5th line of the code snippet. I iterate through the List of revision numbers and call the find method for each of them to get the Book entity that was active at the given revision.

AuditReader auditReader = AuditReaderFactory.get(em);

List revisionNumbers = auditReader.getRevisions(Book.class, b.getId());
for (Number rev : revisionNumbers) {
	Book auditedBook = auditReader.find(Book.class, b.getId(), rev);
	log.info(“Book [“+auditedBook+”] at revision [“+rev+”].”);
}

As you can see in the log messages, Hibernate performs an SQL query to get al revision number for the given entity. The call of the find method triggers another SQL query that returns the record from the audit table which was activate for the given revision number.

10:51:52,378 DEBUG SQL:92 - 
    select
        book_aud0_.REV as col_0_0_ 
    from
        Book_AUD book_aud0_ cross 
    join
        REVINFO defaultrev1_ 
    where
        book_aud0_.id=? 
        and book_aud0_.REV=defaultrev1_.REV 
    order by
        book_aud0_.REV asc
10:51:52,407 DEBUG SQL:92 - 
    select
        book_aud0_.id as id1_3_,
        book_aud0_.REV as REV2_3_,
        book_aud0_.REVTYPE as REVTYPE3_3_,
        book_aud0_.publishingDate as publishi4_3_,
        book_aud0_.title as title5_3_,
        book_aud0_.publisherid as publishe6_3_ 
    from
        Book_AUD book_aud0_ 
    where
        book_aud0_.REV=(
            select
                max(book_aud1_.REV) 
            from
                Book_AUD book_aud1_ 
            where
                book_aud1_.REV<=? 
                and book_aud0_.id=book_aud1_.id
        ) 
        and book_aud0_.REVTYPE<>? 
        and book_aud0_.id=?
10:51:52,418  INFO TestEnvers:118 - Book [Book title: Hibernate Tips] at revision [2].
10:51:52,419 DEBUG SQL:92 - 
    select
        book_aud0_.id as id1_3_,
        book_aud0_.REV as REV2_3_,
        book_aud0_.REVTYPE as REVTYPE3_3_,
        book_aud0_.publishingDate as publishi4_3_,
        book_aud0_.title as title5_3_,
        book_aud0_.publisherid as publishe6_3_ 
    from
        Book_AUD book_aud0_ 
    where
        book_aud0_.REV=(
            select
                max(book_aud1_.REV) 
            from
                Book_AUD book_aud1_ 
            where
                book_aud1_.REV<=? 
                and book_aud0_.id=book_aud1_.id
        ) 
        and book_aud0_.REVTYPE<>? 
        and book_aud0_.id=?
10:51:52,421  INFO TestEnvers:118 - Book [Book title: Hibernate Tips - 64 Tips for your day to day work] at revision [3].

Get active revision at a given date

If you just want to get an entity that was active at a given time, you can call the find method of the AuditReader and provide a java.util.Date instead of a revision number. You can see an example of it in the following code snippet.

AuditReader auditReader = AuditReaderFactory.get(em);

Book auditedBook = auditReader.find(Book.class, b.getId(), created);
log.info(“Book [“+auditedBook+”] at [“+created+”].”);

Hibernate Envers will then perform an SQL query to get the revision number that was active at the given time and perform an additional query to select the record from the audit table.

10:52:52,067 DEBUG SQL:92 - 
    select
        max(defaultrev0_.REV) as col_0_0_ 
    from
        REVINFO defaultrev0_ 
    where
        defaultrev0_.REVTSTMP<=?
10:52:52,117 DEBUG SQL:92 - 
    select
        book_aud0_.id as id1_3_,
        book_aud0_.REV as REV2_3_,
        book_aud0_.REVTYPE as REVTYPE3_3_,
        book_aud0_.publishingDate as publishi4_3_,
        book_aud0_.title as title5_3_,
        book_aud0_.publisherid as publishe6_3_ 
    from
        Book_AUD book_aud0_ 
    where
        book_aud0_.REV=(
            select
                max(book_aud1_.REV) 
            from
                Book_AUD book_aud1_ 
            where
                book_aud1_.REV<=? 
                and book_aud0_.id=book_aud1_.id
        ) 
        and book_aud0_.REVTYPE<>? 
        and book_aud0_.id=?

Summary

Hibernate Envers provides a powerful and easy to use API to write and read audit information. You just need to add the hibernate-envers.jar file to the classpath of your application and annotate your entities with @Audited. Hibernate will then create a new revision for each transaction and create a new record in the audit table for each create, update or delete operation performed on an audited entity.

This post provided only a short introduction to Hibernate Envers. In the following blog posts, I will show you more of the Query API and how you can customize your audit log.


Tags

Configuration, Mapping


About the author

Thorben is an independent consultant, international speaker, and trainer specialized in solving Java persistence problems with JPA and Hibernate.
He is also the author of Amazon’s bestselling book Hibernate Tips - More than 70 solutions to common Hibernate problems.

Books and Courses

Coaching and Consulting

Tools

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.

  1. Amazing, I wanted to create my own history table but that is better than I even imagined.
    Simple tutorial, amazing work @Thorben

    1. Thanks, Petros.
      Adding Envers to your project is a lot easier and faster to implement than writing the audit yourself 😉

{"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}