Help

Recently, I helped one of our customers migrating a legacy database to Hibernate; one of the more interesting topics was versioning and audit logging. Actually, in the last couple of months, the subject of historical data came up several times. No matter if it was a legacy SQL schema or a migration from a broken object-oriented database, everyone had their own way to log data changes.

In this entry, I'll introduce a clean and nice solution for this issue. My proposal naturally integrates with Hibernate. Let's use database triggers and views instead of code in the application layer.

While it is in fact quite easy to write a Hibernate Interceptor for audit logging (an example can be found in Hibernate in Action or on the Hibernate Wiki ), we always like to use the features of the database system. Implementing audit logging in the database is the best choice if many applications share the same schema and data, and usually much less hassle to maintain in the long run.

First, let's create an entity we want to implement a change history for, a simple Item. In Java, this entity is implemented as the Item class. As usual for a Hibernate application that uses Detached Objects and automatic optimistic concurrency control, we give it an id and a version property:

public class Item {

    private Long id = null
    private int version;
    private String description;
    private BigDecimal price;

    Item() {}
    
    ... // Accessor and business methods    
}

This class is then mapped to a table using Hibernate metadata:

<hibernate-mapping>
<class name="Item" table="ITEM_VERSIONED>
    <id name="id" column="ITEM_ID">
        <generator class="native"/>
    </id>
    <version name="version" column="VERSION"/>
    <property name="description" column="DESC"/>
    <property name="price" column="PRICE"/>
</class>
</hibernate-mapping>

The name of the mapped table is ITEM_VERSIONED. This is actually not a normal base table, but a database view that joins the data from two base tables. Let's have a look at the two tables in Oracle:

create table ITEM (
    ITEM_ID    NUMBER(19) NOT NULL,
    DESC       VARCHAR(255) NOT NULL,
    PRICE      NUMBER(19,2) NOT NULL,
    PRIMARY KEY(ITEM_ID)
)

create table ITEM_HISTORY (
    ITEM_ID    NUMBER(19) NOT NULL,
    DESC       VARCHAR(255) NOT NULL,
    PRICE      NUMBER(19,2) NOT NULL,
    VERSION    NUMBER(10) NOT NULL,
    PRIMARY KEY(ITEM_ID, VERSION)
)

The ITEM table is our real entity relation. The ITEM_HISTORY table has a different primary key, using the ITEM_ID and VERSION column. Our goal is to have one row per entity instance in ITEM (the newest version of our data) and one row for each item version in ITEM_HISTORY:

ITEM_ID   DESC            PRICE
1         A nice Item.    123,99
2         Another one.     34,44

ITEM_ID   DESC            PRICE      VERSION
1         The original.   123,99     0
1         An update.      123,99     1
1         A nice Item.    123,99     2
2         Another one.     34,44     0

So, instead of mapping our Java entity to any of the two tables, we map it to a new virtual table, ITEM_VERSIONED. This view merges the data from both base tables:

create or replace view ITEM_VERSIONED (ITEM_ID, VERSION, DESC, PRICE) as
    select I.ITEM_ID as ITEM_ID,
        (select max(IH.VERSION)
            from ITEM_HISTORY HI
            where HI.ITEM_ID = I.ITEM_ID) as VERSION,
        I.DESC as DESC,
        I.PRICE as PRICE
    from   ITEM I

The ITEM_VERSIONED view uses a correlated subquery and a theta-style join to get the highest version number for a particular item from the history table, while selecting the current values from the row in ITEM. Of course we could also directly read all data from ITEM_HISTORY, but this query is more flexible, for example if you don't want to include all original columns in the history.

Hibernate can now read entities and it has a version number for automatic optimistic locking. However, we can not save entities, since the view is read-only. (In Oracle and most other databases, views created using a join can not be updated.) You will get an exception if you try to update an entity.

We solve this problem by writing a database trigger. The trigger will intercept all updates and insertions for the view and redirect the data to the base tables. This kind of trigger is called an /INSTEAD OF/ trigger. Let's first handle insertion:

create or replace trigger ITEM_INSERT
    instead of insert on ITEM_VERSIONED begin
    
    insert into ITEM(ITEM_ID, DESC, PRICE)
           values (:n.ITEM_ID, :n.DESC, :n.PRICE);
           
    insert into ITEM_HISTORY(ITEM_ID, DESC, PRICE, VERSION)
           values (:n.ITEM_ID, :n.DESC, :n.PRICE, :n.VERSION);
end;

This trigger will execute two inserts and split the data between the entity and entity history table. Next, update operations:

create or replace trigger ITEM_UPDATE
    instead of update on ITEM_VERSIONED begin
    
    update ITEM set
            DESC = :n.DESC,
            PRICE = :n.PRICE,
           where
            ITEM_ID = :n.ITEM_ID;
           
    insert into ITEM_HISTORY(ITEM_ID, DESC, PRICE, VERSION)
           values (:n.ITEM_ID, :n.DESC, :n.PRICE, :n.VERSION);
end;

The entity table is updated first, with the new data. Then, a new row is written to the ITEM_HISTORY table.

This is actually all you need to implement a basic history functionality, just check /INSTEAD OF/ trigger support in your database management system. You can even enhance this pattern and make it much more flexible: write a new Auditinfo value type class with user and timestamp information and add an auditinfo property to your entity class in Java. Map this to some new columns in your view using a Hibernate custom UserType and track the information by setting the property in a Hibernate Interceptor when updates and inserts occur. Use AOP to externalize this aspect from your POJOs...

HTH

12 comments:
 
21. Jun 2004, 23:35 CET | Link
Hanson Char
Thanks. This is very interesting.

Any chance you can give an example on how the "Auditinfo" and "Hibernate Interceptor" mentioned at the end of your blog ?
ReplyQuote
 
02. Jul 2004, 18:27 CET | Link
Shouldn't the "version" number be updated to the next value in your "instead of" triggers automatically?
 
13. Jul 2004, 01:47 CET | Link
atorres
I don't like to be bounded to the database, but this may be usefull
 
16. Jul 2004, 22:10 CET | Link
PostgreSQL doesn't seem to support "instead of" triggers but the Rules system seems to do the job as a good substitute. Thanks
 
23. Jul 2004, 15:06 CET | Link
Ralf Huthmann | rah(AT)huthmann.de
If you happen to use Oracle, you should take a look at the 'Workspace Manager'. This tool generates instead-of triggers for selected tables. These tables will then contain only current data, historical data can be acessed using views. It is also possible to set the database to a historical state for all selected tables.
HTH
 
24. Jul 2004, 04:14 CET | Link
Does anyone have any tips on using this pattern with middlegen. The hibernate plugin expects to find realtionships between tables, however the views don't have such relationships.
 
07. Aug 2004, 03:10 CET | Link
Matthew Easlea
Am I right in saying that we must manually create the triggers and view?

Can't hiberante generate the trigger and view in whatever the target database is?
 
06. Jan 2005, 00:52 CET | Link
This is interesting but what do you do if you want to load an old version into your object? For example, imagine this is a financial database and you are versioning trade data. In order to calculate your daily p&l you need to load (and price) the version of the trade as of end-of-day yesterday and as of today.

Even if you can avoid the requirement of having both versions in memory at the same time, you would still need the ability to read them individually.

This is a very common requirement for some types of risk management systems. I would be curious to know if anybody has solved this using Hibernate.

 
18. Jan 2005, 17:56 CET | Link
There is a problem with this design using postgres (7.2 at least). The insert on a view using a rule to do the actual table inserts has a zero return value. However, hibernate (2.1.7) expects the return value to match the number of rows it thought it updated, and it throws an exception. So in 7.2, we have to do all the inserts on a real table and use triggers to keep the tables in synch, while the joining view can only be used for reads.
 
26. Jan 2005, 18:35 CET | Link
Chris
Mark, we are doing something like this by using different session factories that have slightly different mapping files that pull the same objects out of either the 'current' or the 'history table.

There is a problem here in a slightly more complicated example, though. If the entity has an associated array or set, then how does the version of the array or set get assigned? It does not seem easy to refer back to the assigning entity in SQL to make sure the versions are in sync (especially if you are using a timestamp - just putting now() in the rule for the dependant set gives you a time that is slightly different from the entity's, so you can't use id-version as a foreign key)

Anybody have an elegant solution to this? Interceptors perhaps?
 
06. Aug 2006, 07:47 CET | Link
arun
http://blog.hibernate.org/cgi-bin/blosxom.cgi/2004/06/19
 
10. Jun 2008, 23:43 CET | Link

Hello! I found your solution with triggers very nice, but how would you solve the same problem, when the the auditable class has some children objects, which alse are auditable objects, like in a list for example?

Something like this:

<class name="Parent" table="parent">

  <id name="id" type="int" column="id" ><generator class="assigned" /></id>

  <property name="name" type="string" column="name"/>

  <bag name="properties" cascade="all" inverse="true">
    <key column="workstep_id" />
    <one-to-many class="model.WorkstepPropertyValue" />
  </bag>

</class>

Thanks.

Post Comment