This tutorial gives a quick overview of how to get Hibernate Spatial working. We will develop a simple application that stores, and retrieves some simple data objects. The data objects are "special" in that they have a property of type Geometry.
This tutorial assumes that you are familiar with Hibernate, and the basic concepts of working with geographic data. It is also based on the Hibernate Tutorial and uses the same examples. If you haven't read the Hibernate Tutorial, and are new to Hibernate, then please read it before proceeding here.
The Hibernate Tutorial uses the HSQLDB in-memory database to keep things simple. Since Hibernate Spatial doesn't (yet) support HSQLDB, we have no such luck. We require a postgis database. For information on how to create a postgis database, look here .
To use Hibernate Spatial you first need to get Hibernate and the Java Topology Suite. Hibernate provides the ORM functionality, and the Java Topology Suite provides the Geometry Type (and related types). Hibernate Spatial is the glue between them.
You can find Hibernate at http://www.hibernate.org/ . At the Hibernate site you can also find a lot of resources about how to install and use Hibernate. Be sure to get all Hibernate dependencies.
The Java Topology Suite (JTS) can be found at http://www.vividsolutions.com/jts/jtshome.htm . It is a relatively small but rather complete library for all types of Geometries and it conforms to the Simple Feature Specification of the OpenGIS Consortium.
Make sure that you use Hibernate v.3.2 and JTS v.1.8+ (support for Hibernate v3.3 is forthcoming). Also remember that Hibernate Spatial requires a java 1.5+ runtime. We will also assume that you have a working Postgis database ready.
We will create a small application to store and retrieve events we want to attend. (This is the same use case as in the Hibernate Tutorial).
We first set up our development directory and put all the libraries in the ./lib directory. This
is the jts-1.8.jar, hibernate3.jar and the Hibernate dependencies (the rest). Our development directory
now looks like this:
. +lib antlr-2.7.6.jar commons-collections-2.1.1.jar jta.jar asm-attrs.jar commons-logging-1.0.4.jar jts-1.8.jar asm.jar dom4j-1.6.1.jar log4j-1.2.11.jar cglib-2.1.3.jar hibernate3.jar
./lib directory. Specifically, we need the
hibernate-spatial core library, and the provider library for Postgis. We use the (at the time of writing)
latest releases from the download->releases page. Now our development directory looks like:
. +lib antlr-2.7.6.jar ... hibernate3.jar hibernate-spatial-postgis-1.0-M2.jar hibernate-spatial-1.0-M2.jar
Finally, we also need a recent postgresql JDBC Driver together with its postgis extensions. These can be found here and here , resp.
. +lib antlr-2.7.6.jar ... hibernate3.jar hibernate-spatial-postgis-1.0-20070920.111959-1.jar hibernate-spatial-1.0-20070920.111959-1.jar postgis.jar postgresql-8.2-506.jdbc3.jar
Our persistent class is the Event class. Since this class
contains a geometry-valued property (a property of type Geometry), its instances
are geographic objects, or features.
package events;
import java.util.Date;
import com.vividsolutions.jts.geom.Point;
public class Event {
private Long id;
private String title;
private Date date;
private Point location;
public Event() {}
public Long getId() {
return id;
}
private void setId(Long id) {
this.id = id;
}
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public Point getLocation(){
return this.location;
}
public void setLocation(Point location){
this.location = location;
}
}
./src directory of the development directory. So now we have
. +lib +src +event Event.java
To map this to the class, we create the following Hibernate Mapping file.
<hibernate-mapping> <class name="events.Event" table="EVENTS"> <id name="id" column="EVENT_ID"> <generator class="native"/> </id> <property name="date" type="timestamp" column="EVENT_DATE"/> <property name="title"/> <property name="location" type="org.hibernatespatial.GeometryUserType" column="LOC"/> </class> </hibernate-mapping>
GeometryUserType that enables Hibernate to
store the location property properly.
We save this mapping file as Event.hbm.xml along-side the java source file. So now we have
. +lib +src +event Event.java Event.hbm.xml
We proceed with the hibernate configuration file. The only difference
w.r.t. normal Hibernate configurations files is with the dialect
property. Hibernate Spatial extends the Hibernate Dialects so that the spatial
features of the database are available within HQL and the
SpatialCriteria (see below). So instead of using
the (in our case) PostgreSQLDialect, we use Hibernate Spatial's
extension of that dialect which is the PostGISDialect.
Our hibernate.cfg.xml looks like this:
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<!-- Database connection settings -->
<property name="connection.driver_class">org.postgresql.Driver</property>
<property name="connection.url">jdbc:postgresql://localhost:5432/events</property>
<property name="connection.username">postgres</property>
<property name="connection.password"></property>
<!-- JDBC connection pool (use the built-in) -->
<property name="connection.pool_size">1</property>
<!-- SPATIAL SQL dialect -->
<property name="dialect">org.hibernatespatial.postgis.PostgisDialect</property>
<!-- Enable Hibernate's automatic session context management -->
<property name="current_session_context_class">thread</property>
<!-- Disable the second-level cache -->
<property name="cache.provider_class">org.hibernate.cache.NoCacheProvider</property>
<!-- Echo all executed SQL to stdout -->
<property name="show_sql">true</property>
<!-- Drop and re-create the database schema on startup -->
<property name="hbm2ddl.auto">create</property>
<mapping resource="events/Event.hbm.xml"/>
</session-factory>
</hibernate-configuration>
./src directory.
Note that this configuration file means that Hibernate will connect to the "events" database on localhost, with username "postgres" and no password. (on the test system, postgres requires no password). You may need to change these values depending on your set-up.
Also notice that the "hbm2dll.auto" property is activated. This will re-create the database everytime the application is run (more precisely when the Hibernate SessionFactory is run).
To ensure we can quickly build and test and test the application we willl use the Ant build system. You can learn about Ant here . We can use the exact same Ant build file as is used in the Hibernate Tutorial.
<project name="hibernate-tutorial" default="compile">
<property name="sourcedir" value="${basedir}/src"/>
<property name="targetdir" value="${basedir}/bin"/>
<property name="librarydir" value="${basedir}/lib"/>
<path id="libraries">
<fileset dir="${librarydir}">
<include name="*.jar"/>
</fileset>
</path>
<target name="clean">
<delete dir="${targetdir}"/>
<mkdir dir="${targetdir}"/>
</target>
<target name="compile" depends="clean, copy-resources">
<javac srcdir="${sourcedir}"
destdir="${targetdir}"
classpathref="libraries"/>
</target>
<target name="copy-resources">
<copy todir="${targetdir}">
<fileset dir="${sourcedir}">
<exclude name="**/*.java"/>
</fileset>
</copy>
</target>
</project>
build.xml and saved in the root of the development directory.
If we run Ant from this directory, it should compile all sources and put them in the ./bin directory,
together with the Hibernate configuration and mapping files.
HibernatUtil class creates the Hibernate SessionFactory for the application, and provides a getter
to it. (The code below is copied from the Hibernate Tutorial without change).
package util;
import org.hibernate.*;
import org.hibernate.cfg.*;
public class HibernateUtil {
private static final SessionFactory sessionFactory;
static {
try {
// Create the SessionFactory from hibernate.cfg.xml
sessionFactory = new Configuration().configure().buildSessionFactory();
} catch (Throwable ex) {
// Make sure you log the exception, as it might be swallowed
System.err.println("Initial SessionFactory creation failed." + ex);
throw new ExceptionInInitializerError(ex);
}
}
public static SessionFactory getSessionFactory() {
return sessionFactory;
}
}
We are now ready to write a first version of the main application class EventManager.
package events;
import org.hibernate.Session;
import java.util.Date;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.io.WKTReader;
import com.vividsolutions.jts.io.ParseException;
import util.HibernateUtil;
public class EventManager {
public static void main(String[] args) {
EventManager mgr = new EventManager();
if (args[0].equals("store")) {
mgr.createAndStoreEvent("My Event", new Date(), args[1]);
}
HibernateUtil.getSessionFactory().close();
}
private void createAndStoreEvent(String title, Date theDate, String wktPoint ) {
//First interpret the WKT string to a point
WKTReader fromText = new WKTReader();
Geometry geom = null;
try{
geom = fromText.read(wktPoint);
} catch (ParseException e){
throw new RuntimeException("Not a WKT string:" + wktPoint);
}
if (!geom.getGeometryType().equals("Point")){
throw new RuntimeException("Geometry must be a point. Got a " + geom.getGeometryType());
}
Session session = HibernateUtil.getSessionFactory().getCurrentSession();
session.beginTransaction();
Event theEvent = new Event();
theEvent.setTitle(title);
theEvent.setDate(theDate);
theEvent.setLocation((Point)geom);
session.save(theEvent);
session.getTransaction().commit();
}
}
EventManger implementation of the Hibernate Tutorial so that it stores
a point for the event. The point is given as a String in the Well-Known Text (WKT) format. See
the JTS
WTKReader JavaDoc
for more information about the WTK format.
The development directory now looks like this.
. build.xml +lib ...libraries... +bin ... compiled classes ... +src hibernate.cfg.xml +event Event.java Event.hbm.xml EventManager.java +util HibernateUtil.java
To easily run the EventManager we add a callable target to the Ant build file.
<target name="run" depends="compile">
<java fork="true" classname="events.EventManager" classpathref="libraries">
<classpath path="${targetdir}"/>
<arg value="${action}"/>
<arg value="${geom}"/>
</java>
</target>
$ ant run -Daction=store -Dgeom="POINT(10 15)"
events=# select title, astext(loc) from events; title | astext ----------+-------------- My Event | POINT(10 15) (1 row)
We will now modify the EventManager by adding an action to list all events within a certain area.
This will show how to use Hibernate Spatial for spatial querying.
Important: comment out the hbm2ddl.auto property in the hibernate.cfg.xml so that
the event table is not deleted each time we run the EventManager.
Here is the addition to the main method in the EventManager that
implements the "find" action.
...
if (args[0].equals("store")) {
mgr.createAndStoreEvent("My Event", new Date(), args[1]);
}else if (args[0].equals("find")){
List events = mgr.find(args[1]);
for (int i = 0; i < events.size(); i++ ){
Event event = (Event)events.get(i);
System.out.println("Event: " + event.getTitle() +
", Time: " + event.getDate() +
", Location: " + event.getLocation());
}
}
...
find(String filter) method.
The find method takes a WKT string that represents a polygon, and
searches the events table for all events that are located within this polygon.
Here is the code.
...
//New imports
import org.hibernate.Criteria;
import java.util.List;
import org.hibernatespatial.criterion.SpatialRestrictions;
...
private List find(String wktFilter){
WKTReader fromText = new WKTReader();
Geometry filter = null;
try{
filter = fromText.read(wktFilter);
} catch(ParseException e){
throw new RuntimeException("Not a WKT String:" + wktFilter);
}
Session session = HibernateUtil.getSessionFactory().getCurrentSession();
session.beginTransaction();
System.out.println("Filter is : " + filter);
Criteria testCriteria = session.createCriteria(Event.class);
testCriteria.add(SpatialRestrictions.within("location", null, filter));
List results = testCriteria.list();
session.getTransaction().commit();
return results;
}
...
The SpatialRestrictions class provides utility methods to
create Criterion instances for spatial queries. In our
example the WITHIN spatial operator is used to query
all events with a location within a specified filter geometry.
The witin method (as most other methods of SpatialRestrictions) takes
three parameters. The first is the name of the Geometry-valued property on which the filter
is applied; the second is a spatial filter that may be used in a quick spatial-indexed based filter,
and the third is the filter geometry. The second parameter is actually optional and can be set
to null. If you use it, it will make the query much faster if you have a spatial index and the database expects
a specific index-based filter expression (which is the case for PostGIS, but not Oracle). (Don't forget that in this tutorial
we haven't actually created a spatial index!)
If now run:
$ ant run -Daction=find -Dgeom="POLYGON((1 1,20 1,20 20, 1 20, 1 1))"
...
[java] Filter is : POLYGON ((1 1, 20 1, 20 20, 1 20, 1 1))
[java] Hibernate: select this_.EVENT_ID as EVENT1_0_0_, this_.EVENT_DATE as EVENT2_0_0_,
this_.title as title0_0_, this_.LOC as LOC0_0_
from EVENTS this_
where (this_.LOC && ? AND within(this_.LOC, ?))
[java] Event: My Event, Time: 2007-11-01 19:04:08.008, Location: POINT (10 15)
System.out, the
second line is the SQL generated by Hibernate (with the help of Hibernate Spatial).
This is shown because the show_sql property is set in the configuration file.
The last line is the output generated by the find action.
If you examine the generated SQL, you see how the SpatialRestrictions.within
generates the fragment (this_.LOC && ? AND within(this_.LOC, ?)).
The first part is the Postgis "Overlaps" operator that is used for the first stage filter,
the second part is the precise within function as defined by the OGC Simple Features
Specification.
An alternative approach is using HQL. The Postgis Dialect registers functions and operators specified in the the Simple Features for SQL specification. So we could write.
...
Query q = session
.createQuery("from Event where within(location, ?) = true");
Type geometryType = new CustomType(GeometryUserType.class, null);
q.setParameter(0, filter, geometryType);
List result = q.list();
...
Type instance for
the GeometryUserType.
Suppose we want to implement the EventManager on top of Oracle, how do we proceed? We need to change the following items
hibernate-spatial-postgis-*.jar provider with the
hibernate-spatial-oracle-*.jar provider.postgis.jarhibernate.cfg.xml configuration file so that it
points to a suitable Oracle database, uses the Oracle JDBC driver, and has
OracleSpatial10gDialect as dialect.Recent versions of MySQL have (limited) support for spatial types. To get Hibernate Spatial to work with MySQL, change the following items.
hibernate-spatial-postgis-*.jar provider with the
hibernate-spatial-mysql-*.jar provider.postgis.jarhibernate.cfg.xml configuration file so that it
points to a suitable MySQL database, uses the MySQL JDBC driver, and has
MySQLSpatialDialect as dialect.Please note that MySQL's implementation of the spatial types is incomplete at this time. Certain functions of Hibernate Spatial will fail at this time.
The MySQLSpatialDialect extends the vanilla MySQLDialect and therefore only supports the MyISAM tables.
In stead of using *.hbm.xml mapping files, you can also use
JPA annotations. Well almost: JPA doesn't support spatial objects, and doesn't
have an extension mechanism to add new types. So using only JPA annotations won't work.
Fortunately, the Hibernate Annotations implementation of JPA does have a means for mapping
UserTypes.
Let's modify the code to use annotations. First, we need to add the Hibernate Annotations implementation to our lib. We download the Hibernate Annotations package
and put the ejb3-persistence.jar, hibernate-annotations.jar and hibernate-commons-annotations.jar in the lib directory.
So that it becomes:
. +lib antlr-2.7.6.jar commons-collections-2.1.1.jar jta.jar asm-attrs.jar commons-logging-1.0.4.jar jts-1.8.jar asm.jar dom4j-1.6.1.jar ejb3-persistence.jar log4j-1.2.11.jar cglib-2.1.3.jar hibernate3.jar hibernate-commons-annotations.jar hibernate-annotations.jar hibernate-spatial-postgis-1.0-M2.jar hibernate-spatial-1.0-M2.jar
We now change the source code of the Events class so that it looks like this
package events;
import java.util.Date;
import javax.persistence.*;
import org.hibernate.annotations.Type;
import com.vividsolutions.jts.geom.Point;
@Entity
@Table(name="EVENTS")
public class Event {
@Id @GeneratedValue
@Column(name="EVENT_ID")
private Long id;
@Column(name="TITLE")
private String title;
@Column(name="EVENT_DATE")
private Date date;
@Column(name="LOC")
@Type(type="org.hibernatespatial.GeometryUserType")
private Point location;
public Event() {}
public Long getId() {
return id;
}
private void setId(Long id) {
this.id = id;
}
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public Point getLocation(){
return this.location;
}
public void setLocation(Point location){
this.location = location;
}
}
Note that for the Geometry field we needed
to use the Hibernate-specific @Type annotation.
Next, we replace the mapping resource by replacing
the following line from the hibernate.cfg.xml
<mapping resource="events/Event.hbm.xml"/>
with a declaration of mapped classes.
<mapping class="events.Event">
Finally, we change the HibernateUtil so it uses
an AnnotationConfiguration object. Change the line:
... sessionFactory = new Configuration().configure().buildSessionFactory(); ...
needs to change to
... sessionFactory = new AnnotationConfiguration().configure().buildSessionFactory(); ...
Now the operations should work as before.