Hibernate 的空间简介
1. 简介
在本文中,我们将了解 Hibernate 的空间扩展,hibernate-spatial 。
从版本 5 开始,Hibernate Spatial 提供了用于处理地理数据的标准接口。
2. Hibernate Spatial 的背景
地理数据包括Point, Line, Polygon等实体的表示。此类数据类型不是 JDBC 规范的一部分,因此JTS(JTS 拓扑套件) 已成为表示空间数据类型的标准。
除了 JTS,Hibernate spatial 还支持Geolatte-geom - 一个最近的库,它具有一些 JTS 中不可用的功能。
这两个库都已包含在 hibernate-spatial 项目中。使用一个库而不是另一个库只是我们从哪个 jar 导入数据类型的问题。
尽管 Hibernate 空间支持不同的数据库,如 Oracle、MySQL、PostgreSQLql/PostGIS 等,但对数据库特定功能的支持并不统一。
最好参考最新的 Hibernate 文档来检查 hibernate 为给定数据库提供支持的函数列表。
在本文中,我们将使用内存中的Mariadb4j ——它维护 MySQL 的全部功能。
Mariadb4j 和 MySql 的配置相似,甚至 mysql-connector 库也适用于这两个数据库。
3 . Maven 依赖项
让我们看一下设置一个简单的 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>
hibernate-spatial依赖项将为空间数据类型提供支持。最新版本的hibernate-core 、hibernate-spatial 、mysql-connector-java 和mariaDB4j 可以从 Maven Central 获得。
4. 配置 hibernate 空间
第一步是在resources 目录中创建一个hibernate.properties :
hibernate.dialect=org.hibernate.spatial.dialect.mysql.MySQL56SpatialDialect
// ...
唯一特定于 hibernate-spatial 的是MySQL56SpatialDialect 方言。这种方言扩展了MySQL55Dialect方言,并提供了与空间数据类型相关的附加功能。
特定于加载属性文件、创建SessionFactory和实例化 Mariadb4j 实例的代码与标准休眠项目中的代码相同。
5. 了解*Geometry *类型
Geometry 是 JTS 中所有空间类型的基本类型。这意味着Point、Polygon等其他类型从Geometry扩展而来。java中的Geometry类型也对应MySql中的GEOMETRY类型。
通过解析该类型的String表示,我们得到Geometry的一个实例。JTS 提供的实用程序类WKTReader可用于将任何众所周知的文本 表示转换为Geometry类型:
public Geometry wktToGeometry(String wellKnownText)
throws ParseException {
return new WKTReader().read(wellKnownText);
}
现在,让我们看看这个方法的实际效果:
@Test
public void shouldConvertWktToGeometry() {
Geometry geometry = wktToGeometry("POINT (2 5)");
assertEquals("Point", geometry.getGeometryType());
assertTrue(geometry instanceof Point);
}
正如我们所见,即使方法的返回类型是read()方法是Geometry,实际实例也是Point的实例。
6. 在数据库中存储一个点
现在我们对什么是Geometry类型以及如何从String中获取Point有了一个很好的了解,让我们来看看PointEntity:
@Entity
public class PointEntity {
@Id
@GeneratedValue
private Long id;
private Point point;
// standard getters and setters
}
请注意,实体PointEntity包含空间类型Point。如前所述,一个Point由两个坐标表示:
public void insertPoint(String point) {
PointEntity entity = new PointEntity();
entity.setPoint((Point) wktToGeometry(point));
session.persist(entity);
}
方法insertPoint()接受Point的众所周知的文本 (WKT) 表示,将其转换为Point实例,并保存在数据库中。
提醒一下,session不是特定于 hibernate-spatial 的,它的创建方式类似于另一个 hibernate 项目。 我们可以注意到,一旦我们创建了Point的实例,存储PointEntity的过程就类似于任何常规实体。
让我们看一些测试:
@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);
}
在Point上调用toString()会返回Point的 WKT 表示。这是因为Geometry类重写了toString()方法并在内部使用了WKTWriter,这是我们之前看到的WKTReader的一个补充类。
一旦我们运行这个测试,hibernate 将为我们创建PointEntity表。
让我们看一下这张表:
desc PointEntity;
Field Type Null Key
id bigint(20) NO PRI
point geometry YES
正如预期的那样,字段point的Type是GEOMETRY。因此,在使用我们的 SQL 编辑器(如 MySql 工作台)获取数据时,我们需要将此 GEOMETRY 类型转换为人类可读的文本:
select id, astext(point) from PointEntity;
id astext(point)
1 POINT(2 4)
但是,由于当我们在Geometry或其任何子类上调用*toString()*方法时,hibernate 已经返回 WKT 表示,所以我们不需要担心这种转换。
7. 使用空间函数
7.1. *ST_WITHIN()*示例
我们现在来看看使用空间数据类型的数据库函数的用法。
MySQL 中的一个这样的函数是ST_WITHIN(),它告诉一个Geometry是否在另一个 Geometry 中。一个很好的例子是找出给定半径内的所有点。
让我们先来看看如何创建一个圆圈:
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();
}
圆由*setNumPoints()方法指定的一组有限点表示。在调用setSize()*方法之前,*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 将其 inside() 函数映射到MySql的*ST_WITHIN()*函数。
这里一个有趣的观察是点 (3, 4) 正好落在圆上。尽管如此,查询并没有返回这一点。这是因为仅当给定的Geometry完全在另一个Geometry内时,within()函数才返回 true。
7.2. *ST_TOUCHES()*示例
在这里,我们将展示一个在数据库中插入一组Polygon并选择与给定Polygon相邻的Polygon的示例。让我们快速看一下PolygonEntity类:
@Entity
public class PolygonEntity {
@Id
@GeneratedValue
private Long id;
private Polygon polygon;
// standard getters and setters
}
这里与之前的PointEntity唯一不同的是我们使用类型Polygon而不是Point。
现在让我们进行测试:
@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))");
}
*insertPolygon()方法类似于我们之前看到的insertPoint()*方法。
我们使用touches()函数来查找与给定 Polygon 相邻的Polygon。显然,第三个Polygon不会在结果中返回,因为没有边缘接触给定的Polygon。