1. Introduction
1.介绍
In this article, we’ll have a look a the spatial extension of Hibernate, hibernate-spatial.
在这篇文章中,我们将看一下Hibernate的空间扩展,hibernate-spatial。
Starting with version 5, Hibernate Spatial provides a standard interface for working with geographic data.
从版本5开始,Hibernate Spatial为处理地理数据提供了一个标准接口。
2. Background on Hibernate Spatial
2.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.
地理数据包括像点、线、多边形等实体的表示。这种数据类型不是JDBC规范的一部分,因此JTS(JTS Topology Suite)已经成为表示空间数据类型的标准。
Apart from JTS, Hibernate spatial also supports Geolatte-geom – a recent library that has some features that aren’t available in JTS.
除了JTS之外,Hibernate空间还支持Geolatte-geom–一个最新的库,它有一些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.
这两个库都已经包含在hibernate-spatial项目中。使用一个库而不是另一个库只是一个问题,即我们从哪个jar中导入数据类型。
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.
尽管Hibernate空间支持不同的数据库,如Oracle、MySQL、PostgreSQLql/PostGIS和其他一些数据库,但对数据库特定功能的支持并不统一。
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.
最好是参考最新的Hibernate文档,查看hibernate为特定数据库提供支持的函数列表。
In this article, we’ll be using an in-memory Mariadb4j – which maintains the full functionality of MySQL.
在本文中,我们将使用内存中的Mariadb4j – 它保持了MySQL的全部功能。
The configuration for Mariadb4j and MySql are similar, even the mysql-connector library works for both of these databases.
Mariadb4j和MySql的配置是相似的,甚至mysql-connector库也适用于这两个数据库。
3. Maven Dependencies
3.Maven的依赖性
Let’s have a look at the Maven dependencies required for setting up a simple hibernate-spatial project:
让我们看看建立一个简单的hibernate-spatial项目所需的Maven依赖项。
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>5.2.12.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-spatial</artifactId>
<version>5.2.12.Final</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>6.0.6</version>
</dependency>
<dependency>
<groupId>ch.vorburger.mariaDB4j</groupId>
<artifactId>mariaDB4j</artifactId>
<version>2.2.3</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.
hibernate-spatial依赖关系是将提供对空间数据类型的支持。hibernate-core、hibernate-spatial、mysql-connector-java和mariaDB4j的最新版本可以从Maven Central获得。
4. Configuring Hibernate Spatial
4.配置Hibernate Spatial
The first step is to create a hibernate.properties in the resources directory:
第一步是在resources目录下创建一个hibernate.properties。
hibernate.dialect=org.hibernate.spatial.dialect.mysql.MySQL56SpatialDialect
// ...
The only thing that is specific to hibernate-spatial is the MySQL56SpatialDialect dialect. This dialect extends the MySQL55Dialect dialect and provides additional functionality related to the spatial data types.
唯一针对hibernate-spatial的是MySQL56SpatialDialect方言。这个方言扩展了MySQL55Dialect方言,并提供了与空间数据类型相关的额外功能。
The code specific to loading the property file, creating a SessionFactory, and instantiating a Mariadb4j instance, is same as in a standard hibernate project.
加载属性文件、创建SessionFactory和实例化Mariadb4j实例的具体代码与标准hibernate项目中的相同。
5. Understanding the Geometry Type
5.了解Geometry类型
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.
Geometry是JTS中所有空间类型的基础类型。这意味着像Point、Polygon等其他类型都是从Geometry扩展而来。java中的Geometry类型与MySql中的GEOMETRY类型也是对应的。
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:
通过解析该类型的String表示法,我们得到一个Geometry的实例。JTS提供的一个实用类WKTReader可以用来将任何知名的文本表示法转换为Geometry类型。
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.
我们可以看到,即使方法的返回类型是read()方法是Geometry,实际实例是Point。
6. Storing a Point in DB
6.在数据库中存储一个点
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:
现在我们对什么是Geometry类型以及如何从String中获得Point有了很好的了解,让我们看看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:
请注意,实体PointEntity包含一个空间类型Point。正如前面所展示的,一个Point由两个坐标表示。
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.
方法insertPoint()接受Point的知名文本(WKT)表示,将其转换为Point实例,并保存在DB中。
As a reminder, the session isn’t specific to hibernate-spatial and is created in a way similar to another hibernate project.
作为提醒,session并不是特定于hibernate-spatial的,它的创建方式与另一个hibernate项目类似。
We can notice here that once we have an instance of Point created, the process of storing PointEntity is similar to any regular entity.
我们在这里可以注意到,一旦我们创建了Point的实例,存储PointEntity的过程就与任何常规实体类似。
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.
在Point上调用toString()会返回Point的WKT表示。这是因为Geometry类重写了toString()方法,并在内部使用WKTWriter,这是我们之前看到的WKTReader的补充类。
Once we run this test, hibernate will create PointEntity table for us.
一旦我们运行这个测试,hibernate将为我们创建PointEntity表。
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:
正如预期,Field Point的Type是GEOMETRY。正因为如此,在使用我们的SQL编辑器(如MySql workbench)获取数据时,我们需要将这个GEOMETRY类型转换为人类可读的文本。
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.
然而,当我们在Geometry或其任何子类上调用toString()方法时,hibernate已经返回WKT表示,我们不需要为这种转换而烦恼。
7. Using Spatial Functions
7.使用空间函数
7.1. ST_WITHIN() Example
7.1.ST_WITHIN() 示例
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.
在MySQL中,其中一个函数是ST_WITHIN(),它告诉一个几何体是否在另一个几何体之内。这里有一个很好的例子,就是找出一个给定半径内的所有点。
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.
圆是由setNumPoints()方法所指定的一组有限的点来表示。在调用setSize()方法之前,radius被加倍,因为我们需要在两个方向上围绕中心绘制圆。
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.
Hibernate将其within()函数映射到MySql的ST_WITHIN()函数。
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.
一个有趣的观察是,点(3,4)正好落在圆上。但是,查询并没有返回这个点。这是因为within()函数只有在给定的Geometry完全在另一个Geometry内时才返回真。
7.2. ST_TOUCHES() Example
7.2.ST_TOUCHES() 示例
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:
在这里,我们将介绍一个例子,在数据库中插入一组Polygons,并选择与给定Polygon相邻的Polygons。让我们快速浏览一下PolygonEntity类。
@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.
这里与之前的PointEntity唯一不同的是,我们使用Polygon类型而不是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.
insertPolygon()方法与我们之前看到的insertPoint()方法类似。源码中包含了这个方法的完整实现。
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.
我们使用touches()函数来查找与给定Polygon相邻的Polygon。显然,第三个Polygon不会在结果中返回,因为没有边接触到给定的Polygon。
8. Conclusion
8.结语
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.
在这篇文章中,我们已经看到hibernate-spatial使处理空间数据类型变得简单多了,因为它处理了低层次的细节。
Even though this article uses Mariadb4j, we can replace it with MySql without modifying any configuration.
尽管本文使用了Mariadb4j,但我们可以用MySql代替它,而不需要修改任何配置。
As always, the full source code for this article can be found over on GitHub.
一如既往,本文的完整源代码可以在GitHub上找到。