1. Introduction
In this article, we’ll have a look a the spatial extension of Hibernate, hibernate-spatial.
Starting with version 5, Hibernate Spatial provides a standard interface for working with geographic data.
2. Background on Hibernate Spatial
Geographic data includes representation of entities like a Point, Line, Polygon. Such data types aren’t a part of the JDBC specification, hence the JTS (JTS Topology Suite) has become a standard for representing spatial data types.
Apart from JTS, Hibernate spatial also supports Geolatte-geom – a recent library that has some features that aren’t available in JTS.
Both libraries are already included in the hibernate-spatial project. Using one library over other is simply a question of from which jar we’re importing data types.
Although Hibernate spatial supports different databases like Oracle, MySQL, PostgreSQLql/PostGIS, and a few others, the support for the database specific functions isn’t uniform.
It’s better to refer to the latest Hibernate documentation to check the list of functions for which hibernate provides support for a given database.
In this article, we’ll be using an in-memory Mariadb4j – which maintains the full functionality of MySQL.
The configuration for Mariadb4j and MySql are similar, even the mysql-connector library works for both of these databases.
3**. Maven Dependencies**
Let’s have a look at the Maven dependencies required for setting up a simple hibernate-spatial project:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>6.4.2.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-spatial</artifactId>
<version>6.4.2.Final</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.32</version>
</dependency>
<dependency>
<groupId>ch.vorburger.mariaDB4j</groupId>
<artifactId>mariaDB4j</artifactId>
<version>2.6.0</version>
</dependency>
The hibernate-spatial dependency is the one that will provide support for the spatial data types. The latest versions of hibernate-core, hibernate-spatial, mysql-connector-java, and mariaDB4j can be obtained from Maven Central.
4. Configuring Hibernate Spatial
The first step is to create a hibernate.properties in the resources directory:
hibernate.dialect=org.hibernate.dialect.H2Dialect
// ...
We will use the same dialect for the usual H2 database which is H2Dialect. This by default provides additional functionality related to the spatial data types.
The code specific to loading the property file, creating a SessionFactory, and instantiating a Mariadb4j instance, is same as in a standard hibernate project.
5**. Understanding the Geometry Type**
Geometry is the base type for all the spatial types in JTS. This means that other types like Point, Polygon, and others extend from Geometry. The Geometry type in java corresponds to the GEOMETRY type in MySql as well.
By parsing a String representation of the type, we get an instance of Geometry. A utility class WKTReader provided by JTS can be used to convert any well-known text representation to a Geometry type:
public Geometry wktToGeometry(String wellKnownText)
throws ParseException {
return new WKTReader().read(wellKnownText);
}
Now, let’s see this method in action:
@Test
public void shouldConvertWktToGeometry() {
Geometry geometry = wktToGeometry("POINT (2 5)");
assertEquals("Point", geometry.getGeometryType());
assertTrue(geometry instanceof Point);
}
As we can see, even if the return type of the method is read() method is Geometry, the actual instance is that of a Point.
6. Storing a Point in DB
Now that we have a good idea of what a Geometry type is and how to get a Point out of a String, let’s have a look at the PointEntity:
@Entity
public class PointEntity {
@Id
@GeneratedValue
private Long id;
private Point point;
// standard getters and setters
}
Note that the entity PointEntity contains a spatial type Point. As demonstrated earlier, a Point is represented by two coordinates:
public void insertPoint(String point) {
PointEntity entity = new PointEntity();
entity.setPoint((Point) wktToGeometry(point));
session.persist(entity);
}
The method insertPoint() accepts a well-known text (WKT) representation of a Point, converts it to a Point instance, and saves in the DB.
As a reminder, the session isn’t specific to hibernate-spatial and is created in a way similar to another hibernate project.
We can notice here that once we have an instance of Point created, the process of storing PointEntity is similar to any regular entity.
Let’s look at some tests:
@Test
public void shouldInsertAndSelectPoints() {
PointEntity entity = new PointEntity();
entity.setPoint((Point) wktToGeometry("POINT (1 1)"));
session.persist(entity);
PointEntity fromDb = session
.find(PointEntity.class, entity.getId());
assertEquals("POINT (1 1)", fromDb.getPoint().toString());
assertTrue(geometry instanceof Point);
}
Calling toString() on a Point returns the WKT representation of a Point. This is because the Geometry class overrides the toString() method and internally uses WKTWriter, a complimentary class to WKTReader that we saw earlier.
Once we run this test, hibernate will create PointEntity table for us.
Let’s have a look at that table:
desc PointEntity;
Field Type Null Key
id bigint(20) NO PRI
point geometry YES
As expected, the Type of Field Point is GEOMETRY. Because of this, while fetching the data using our SQL editor (like MySql workbench), we need to convert this GEOMETRY type to human-readable text:
select id, astext(point) from PointEntity;
id astext(point)
1 POINT(2 4)
However, as hibernate already returns WKT representation when we call toString() method on Geometry or any of its subclasses, we don’t need to bother about this conversion.
7. Using Spatial Functions
7.1. ST_WITHIN() Example
We’ll now have a look at the usage of database functions that work with spatial data types.
One of such function in MySQL is ST_WITHIN() that tells whether one Geometry is within another. A good example here would be to find out all the points within a given radius.
Let’s start by looking at how to create a circle:
public Geometry createCircle(double x, double y, double radius) {
GeometricShapeFactory shapeFactory = new GeometricShapeFactory();
shapeFactory.setNumPoints(32);
shapeFactory.setCentre(new Coordinate(x, y));
shapeFactory.setSize(radius * 2);
return shapeFactory.createCircle();
}
A circle is represented by a finite set of points specified by the setNumPoints() method. The radius is doubled before calling the setSize() method as we need to draw the circle around the center, in both the directions.
Let’s now move forward and see how to fetch the points within a given radius:
@Test
public void shouldSelectAllPointsWithinRadius() throws ParseException {
insertPoint("POINT (1 1)");
insertPoint("POINT (1 2)");
insertPoint("POINT (3 4)");
insertPoint("POINT (5 6)");
Query query = session.createQuery("select p from PointEntity p where
within(p.point, :circle) = true", PointEntity.class);
query.setParameter("circle", createCircle(0.0, 0.0, 5));
assertThat(query.getResultList().stream()
.map(p -> ((PointEntity) p).getPoint().toString()))
.containsOnly("POINT (1 1)", "POINT (1 2)");
}
Hibernate maps its within() function to the ST_WITHIN() function of MySql.
An interesting observation here is that the Point (3, 4) falls exactly on the circle. Still, the query doesn’t return this point. This is because the within() function returns true only if the given Geometry is completely within another Geometry.
7.2. ST_TOUCHES() Example
Here, we’ll present an example that inserts a set of Polygons in the database and select the Polygons that are adjacent to a given Polygon. Let’s have a quick look at the PolygonEntity class:
@Entity
public class PolygonEntity {
@Id
@GeneratedValue
private Long id;
private Polygon polygon;
// standard getters and setters
}
The only thing different here from the previous PointEntity is that we’re using the type Polygon instead of the Point.
Let’s now move towards the test:
@Test
public void shouldSelectAdjacentPolygons() throws ParseException {
insertPolygon("POLYGON ((0 0, 0 5, 5 5, 5 0, 0 0))");
insertPolygon("POLYGON ((3 0, 3 5, 8 5, 8 0, 3 0))");
insertPolygon("POLYGON ((2 2, 3 1, 2 5, 4 3, 3 3, 2 2))");
Query query = session.createQuery("select p from PolygonEntity p
where touches(p.polygon, :polygon) = true", PolygonEntity.class);
query.setParameter("polygon", wktToGeometry("POLYGON ((5 5, 5 10, 10 10, 10 5, 5 5))"));
assertThat(query.getResultList().stream()
.map(p -> ((PolygonEntity) p).getPolygon().toString())).containsOnly(
"POLYGON ((0 0, 0 5, 5 5, 5 0, 0 0))", "POLYGON ((3 0, 3 5, 8 5, 8 0, 3 0))");
}
The insertPolygon() method is similar to the insertPoint() method that we saw earlier. The source contains the full implementation of this method.
We’re using the touches() function to find the Polygons adjacent to a given Polygon. Clearly, the third Polygon is not returned in the result as there is not edge touching the given Polygon.
8. Conclusion
In this article, we’ve seen that hibernate-spatial makes dealing with spatial datatypes a lot simpler as it takes care of the low-level details.
Even though this article uses Mariadb4j, we can replace it with MySql without modifying any configuration.
As always, the full source code for this article can be found over on GitHub.