Hibernate Spatial and Envers – Geospatial revisions
Especially in social software, where almost everybody can edit the contents like in Wikipedia, it is important to store the history of the data. OpenStreetMap is also working on a concept of changesets. Some time ago, I was looking for such functionality that can be used in conjunction with the Java Persistence API (JPA) and automatically logs changes. Perhaps you already noticed the Blog of Adam Warksi at my blogroll, who is the founder of the Envers project.
The Envers project aims to enable easy versioning of persistent classes. All that you have to do is annotate your persistent class or some of its properties, that you want to version, with @Audited. For each versioned entity, a table will be created, which will hold the history of changes made to the entity. You can then retrieve and query historical data without much effort. Similarly to Subversion, the library has a concept of revisions. Basically, one transaction is one revision (unless the transaction didn’t modify any versioned entities). As the revisions are global, having a revision number, you can query for various entities at that revision, retrieving a (partial) view of the database at that revision. You can find a revision number having a date, and the other way round, you can get the date at which a revision was commited. [Envers project]
Envers works on top of / integrates into hibernate, which is an implementation of the JPA specification integrated in the JBoss Application Server and since october 2008, envers is an official hibernate module.
My first attempts getting envers work, were very disappointing, since I got many exceptions, after I added envers to a well working project. Yesterday, I made a new attempt and downloaded snapshot versions of hibernate (until now, there is no official release of hibernate, which contains envers) from the MavenSnapshotReporitory. My code was working fine and envers created a new revision for my data for every commit. Really nice! Great job, Adam!
But what about Hibernate Spatial (like in my post about storing map data) and Envers? Do they cooperate? I was dying to write a little test, whether that works. First I, created a class, that stores markers, that have got an id, a caption and a location:
package com.claudiushauptmann.test;
import java.io.Serializable;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import org.hibernate.annotations.Type;
import org.hibernate.envers.Audited;
import com.vividsolutions.jts.geom.Point;
@Entity
@Audited
public class Marker implements Serializable {
@Id
@GeneratedValue
private int id;
@Type(type="org.hibernatespatial.GeometryUserType")
private Point location;
private String caption;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public Point getLocation() {
return location;
}
public void setLocation(Point location) {
this.location = location;
}
public String getCaption() {
return caption;
}
public void setCaption(String caption) {
this.caption = caption;
}
}
Second, I created two tables, one for the current data and one for the history:
CREATE TABLE marker ( id integer NOT NULL, "location" geometry NOT NULL, caption text NOT NULL, CONSTRAINT "PK_marker" PRIMARY KEY (id) ) WITH (OIDS=FALSE);CREATE TABLE marker_aud ( id integer NOT NULL, "location" geometry NOT NULL, caption text NOT NULL, rev integer NOT NULL, revtype integer NOT NULL ) WITH (OIDS=FALSE);
Third, there must be a table, that contains meta data about the revisions. This is the minimum of the columns:
CREATE TABLE revinfo ( rev integer NOT NULL, revtstmp bigint NOT NULL, CONSTRAINT "PK_REVINFO" PRIMARY KEY (rev) ) WITH (OIDS=FALSE);
Don’t forget to add enlist envers in your persistence.xml file:
<?xml version="1.0" encoding="UTF-8"?> <persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"> <persistence-unit name="test"> <jta-data-source>java:/test</jta-data-source> <properties> <property name="hibernate.dialect" value="org.hibernatespatial.postgis.PostgisDialect" /> <property name="hibernate.show_sql" value="true" /> <property name="hibernate.ejb.event.post-insert" value="org.hibernate.envers.event.AuditEventListener" /> <property name="hibernate.ejb.event.post-update" value="org.hibernate.envers.event.AuditEventListener" /> <property name="hibernate.ejb.event.post-delete" value="org.hibernate.envers.event.AuditEventListener" /> <property name="hibernate.ejb.event.pre-collection-update" value="org.hibernate.envers.event.AuditEventListener" /> <property name="hibernate.ejb.event.pre-collection-remove" value="org.hibernate.envers.event.AuditEventListener" /> <property name="hibernate.ejb.event.post-collection-recreate" value="org.hibernate.envers.event.AuditEventListener" /> <property name="org.jboss.envers.warnOnUnsupportedTypes" value="true" /> </properties> </persistence-unit> </persistence>
This is working very well! For every commit, a new revision is created in the table revinfo and the new version of the marker is stored into the table marker_aud and the data within the table marker was updated.
Of course, we do not only need to store the history, but also a way to retrieve past data. There is a pretty nice quickstart tutorial about envers, which describes, how to get the data out of the history with the VersionsReader.
4 Comments »
RSS feed for comments on this post. TrackBack URL
Hi – could you provide the code for this example? Or at least the portion of code that shows query/storage via the EntityManager.
I am able to get this working (ignoring envers) using pure hibernate but I would like to get it working using JPA instead.
Currently, I am encountering a serialisation problem when using the simplest of queries on a table which has a Geometry column e.g.
Query query = em.createQuery(“select b from Branch b”);
results in
javax.persistence.PersistenceException: org.hibernate.type.SerializationException: could not deserialize
…
Caused by: java.io.StreamCorruptedException: invalid stream header: 30313031
Any help would be much appreciated.
Thanks in advance,
Damian
If anybody stumbles upon your blog with the same problem, I managed to solve the above by placing my @Type(type=”org.hibernatespatial.GeometryUserType”) annotation above my getLocation() method instead of over the location property in my Entity.
Still, I’d be interested to see how you coded spatial queries using JPA Claudius!
Great blog BTW.
Hi Damian, thank you for your comments! You can’t use JPA directly since JPA does not support extensions like HibernateSpatial, thus you have to ask your Query for the HibernateQuery and then you can add parameters of GeometryUserType:
GeometryFactory geometryFactory = new GeometryFactory();
WKTReader reader = new WKTReader(geometryFactory);
Polygon polygon = (Polygon) reader.read(“POLYGON((” + north + ” ”
+ west + “, ” + north + ” ” + east + “, ” + south + ” ”
+ east + “, ” + south + ” ” + west + “, ” + north + ” ”
+ west + “))”);
polygon.setSRID(31466);
Query q = em
.createQuery(“SELECT gf FROM GeonameFeature AS gf WHERE ((gf.featureClass=’T’ AND (gf.featureCode=’MT’ OR gf.featureCode=’PK’)) ) AND true = ST_within(gf.location, :within)”);
org.hibernate.ejb.QueryImpl hs = (QueryImpl) q;
org.hibernate.Query hbQuery = hs.getHibernateQuery();
hbQuery.setParameter(“within”, polygon, org.hibernate.Hibernate
.custom(org.hibernatespatial.GeometryUserType.class));
q.setParameter(“within”, polygon); gfs = q.getResultList();
q.setMaxResults(200);
List
Works beautifully! Thank you so much Claudius