1. 序章

この記事では、Hibernateの空間拡張hibernate-spatialを見ていきます。

バージョン5以降、 Hibernate Spatialは、地理データを操作するための標準インターフェースを提供します。

2. HibernateSpatialの背景

地理データには、ポイント、ライン、ポリゴンなどのエンティティの表現が含まれます。 このようなデータ型はJDBC仕様の一部ではないため、 JTS(JTS Topology Suite)は空間データ型を表すための標準になりました。

JTSとは別に、HibernateSpatialはGeolatte-geom もサポートしています。これは、JTSでは利用できないいくつかの機能を備えた最近のライブラリです。

両方のライブラリは、Hibernate-Spatialプロジェクトにすでに含まれています。 あるライブラリを他のライブラリよりも使用することは、データ型をインポートするjarの問題です。

Hibernate Spatialは、Oracle、MySQL、PostgreSQLql / PostGISなどのさまざまなデータベースをサポートしていますが、データベース固有の機能のサポートは統一されていません。

最新のHibernateのドキュメントを参照して、hibernateが特定のデータベースのサポートを提供する関数のリストを確認することをお勧めします。

この記事では、MySQLの全機能を維持するインメモリMariadb4jを使用します。

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の最新バージョンはMavenCentralから入手できます。 。

4. HibernateSpatialの構成

最初のステップは、resourcesディレクトリにhibernate.propertiesを作成することです。

hibernate.dialect=org.hibernate.spatial.dialect.mysql.MySQL56SpatialDialect
// ...

hibernate-spatialに固有の唯一のものは、MySQL56SpatialDialect方言です。 この方言は、 MySQL55Dialect 方言を拡張し、空間データ型に関連する追加機能を提供します。

プロパティファイルのロード、 SessionFactory の作成、およびMariadb4jインスタンスのインスタンス化に固有のコードは、標準の休止状態プロジェクトの場合と同じです。

5 。 ジオメトリタイプを理解する

Geometry は、JTSのすべての空間タイプの基本タイプです。 これは、 Point Polygon などの他のタイプが、Geometryから拡張されていることを意味します。 javaのGeometryタイプは、MySqlのGEOMETRYタイプにも対応しています。

タイプのString表現を解析することにより、Geometryのインスタンスを取得します。 JTSが提供するユーティリティクラスWKTReaderを使用して、 well-knowntext表現を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. DBにポイントを保存する

Geometry タイプとは何か、StringからPointを取得する方法がわかったので、を見てみましょう。 ] PointEntity

@Entity
public class PointEntity {

    @Id
    @GeneratedValue
    private Long id;

    private Point point;

    // standard getters and setters
}

エンティティPointEntityには空間タイプPointが含まれていることに注意してください。 前に示したように、Pointは2つの座標で表されます。

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

メソッドinsertPoint()は、 Point のWell-KnownText(WKT)表現を受け入れ、それを Point インスタンスに変換し、DBに保存します。

念のため、セッションは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);
}

PointtoString()を呼び出すと、PointのWKT表現が返されます。 これは、 GeometryクラスがtoString()メソッドをオーバーライドし、前に見たWKTReaderの補完クラスであるWKTWriterを内部的に使用するためです。 。

このテストを実行すると、hibernateはPointEntityテーブルを作成します。

その表を見てみましょう。

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

予想どおり、 Field PointTypeGEOMETRYです。 このため、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のそのような関数の1つは、1つの Geometryが別のGeometry内にあるかどうかを示すST_WITHIN()です。 ここでの良い例は、特定の半径内のすべてのポイントを見つけることです。

円を作成する方法を見てみましょう。

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()メソッドで指定された有限の点のセットで表されます。 radius は、 setSize()メソッドを呼び出す前に倍増されます。これは、中心の周りに両方向に円を描く必要があるためです。

次に進んで、指定された半径内のポイントをフェッチする方法を見てみましょう。

@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()

ここでは、データベースに Polygon のセットを挿入し、特定のPolygonに隣接するPolygonを選択する例を示します。 PolygonEntityクラスを簡単に見てみましょう。

@Entity
public class PolygonEntity {

    @Id
    @GeneratedValue
    private Long id;

    private Polygon polygon;

    // standard getters and setters
}

ここで以前のPointEntityと異なるのは、Pointの代わりにPolygonタイプを使用していることだけです。

それでは、テストに移りましょう。

@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を検索しています。 明らかに、3番目のポリゴンは、指定されたポリゴンに接触するエッジがないため、結果に返されません。

8. 結論

この記事では、hibernate-spatialを使用すると、低レベルの詳細を処理するため、空間データ型の処理が非常に簡単になることを確認しました。

この記事ではMariadb4jを使用していますが、構成を変更せずにMySqlに置き換えることができます。

いつものように、この記事の完全なソースコードは、GitHubにあります。