2. Hibernate Spatial 背景

Hibernate Spatial 是 Hibernate 的空间扩展,从版本5开始,它提供了操作地理空间数据的标准化接口

地理空间数据包含点(Point)、线(Line)、面(Polygon)等实体的表示。这些数据类型不在 JDBC 规范范围内,因此 JTS (JTS Topology Suite) 成为表示空间数据的事实标准。

除 JTS 外,Hibernate Spatial 还支持 Geolatte-geom —— 一个提供 JTS 缺失功能的新兴库。

两个库已集成在 hibernate-spatial 项目中,选择哪个库仅取决于导入数据类型的来源。

尽管 Hibernate Spatial 支持 Oracle、MySQL、PostgreSQL/PostGIS 等数据库,但对特定数据库函数的支持并不统一。建议查阅最新版 Hibernate 文档确认各数据库支持的函数列表。

本文使用内存数据库 Mariadb4j(保留 MySQL 全功能),其配置与 MySQL 兼容,甚至可直接使用 mysql-connector 驱动。

3. Maven 依赖

搭建基础 hibernate-spatial 项目需添加以下 Maven 依赖:

<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>

关键依赖 hibernate-spatial 提供空间数据类型支持。最新版本可通过 Maven Central 获取。

4. 配置 Hibernate Spatial

resources 目录创建 hibernate.properties

hibernate.dialect=org.hibernate.dialect.H2Dialect
// ...

直接使用 H2 数据库的方言 H2Dialect,该方言默认包含空间数据类型支持。

属性文件加载、SessionFactory 创建和 Mariadb4j 实例化与标准 Hibernate 项目完全一致。

5. 理解 Geometry 类型

Geometry 是 JTS 所有空间类型的基类,PointPolygon 等类型均继承自它。Java 中的 Geometry 对应 MySQL 的 GEOMETRY 类型。

通过解析字符串表示可获取 Geometry 实例,JTS 提供的 WKTReader 工具类能将 well-known text(WKT)转换为 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 类型后,我们定义 PointEntity

@Entity
public class PointEntity {

    @Id
    @GeneratedValue
    private Long id;

    private Point point;

    // 标准getter/setter
}

实体包含空间类型 Point,由两个坐标表示:

public void insertPoint(String point) {
    PointEntity entity = new PointEntity();
    entity.setPoint((Point) wktToGeometry(point));
    session.persist(entity);
}

insertPoint() 接收 WKT 格式的点,转换后存入数据库。

✅ 关键:Point 实例化后,存储流程与常规实体完全一致。

测试用例:

@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);
}

调用 toString() 返回 WKT 表示,因 Geometry 类内部使用 WKTWriter(与 WKTReader 配对)。

测试执行后自动创建的表结构:

desc PointEntity;
Field    Type          Null    Key
id       bigint(20)    NO      PRI
point    geometry      YES

✅ 验证:point 字段类型为 GEOMETRY。在 SQL 客户端查询时需转换格式:

select id, astext(point) from PointEntity;

id      astext(point)
1       POINT(2 4)

但 Hibernate 的 toString() 已处理转换,无需手动干预。

7. 使用空间函数

7.1. ST_WITHIN() 示例

演示空间函数用法,以 MySQL 的 ST_WITHIN() 为例,用于判断几何对象是否在指定范围内(如查找半径内的点)。

创建圆形几何对象:

public Geometry createCircle(double x, double y, double radius) {
    GeometricShapeFactory shapeFactory = new GeometricShapeFactory();
    shapeFactory.setNumPoints(32); // 用32个点近似圆形
    shapeFactory.setCentre(new Coordinate(x, y));
    shapeFactory.setSize(radius * 2); // 半径需×2(双向扩展)
    return shapeFactory.createCircle();
}

查询指定半径内的点:

@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 将 within() 映射到 MySQL 的 ST_WITHIN() 函数。

❌ 踩坑:点 (3,4) 在圆周上但未被返回,因 within() 仅当几何对象完全在目标内部时返回 true

7.2. ST_TOUCHES() 示例

插入多边形并查询相邻多边形(边接触)。定义 PolygonEntity

@Entity
public class PolygonEntity {

    @Id
    @GeneratedValue
    private Long id;

    private Polygon polygon;

    // 标准getter/setter
}

唯一区别是使用 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() 查找相邻多边形,第三个多边形因无接触边被过滤。

8. 总结

Hibernate Spatial 通过封装底层细节,极大简化了空间数据操作。本文使用 Mariadb4j,但可无缝替换为 MySQL。

完整代码示例见 GitHub 仓库


原始标题:Introduction to Hibernate Spatial

« 上一篇: Java每周,问题207