Hibernate Spatialの紹介

Hibernate Spatialの概要

1. 前書き

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

バージョン5以降、Hibernate Spatial provides a standard interface for working with geographic data

2. Hibernate Spatialの背景

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

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

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

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

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

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

Mariadb4jとMySqlの構成は類似しており、mysql-connectorライブラリーでもこれらのデータベースの両方で機能します。

3. Maven Dependencies

単純なHibernate-Spatialプロジェクトをセットアップするために必要なMavenの依存関係を見てみましょう。


    org.hibernate
    hibernate-entitymanager
    5.2.12.Final


    org.hibernate
    hibernate-spatial
    5.2.12.Final


    mysql
    mysql-connector-java
    6.0.6


    ch.vorburger.mariaDB4j
    mariaDB4j
    2.2.3

hibernate-spatial依存関係は、空間データ型のサポートを提供する依存関係です。 hibernate-entitymanagerhibernate-spatialmysql-connector-java、およびmariaDB4jの最新バージョンは、MavenCentralから入手できます。

4. Hibernate Spatialの構成

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

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

The only thing that is specific to hibernate-spatial is the MySQL56SpatialDialect dialect。 この方言は、MySQL55Dialect方言を拡張し、空間データ型に関連する追加機能を提供します。

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

5. Understanding the Geometry Type

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

タイプのString表現を解析することにより、Geometryのインスタンスを取得します。 JTSによって提供されるユーティリティクラスWKTReaderを使用して、任意のwell-known text表現を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-Known Text(WKT)表現を受け入れ、それをPointインスタンスに変換して、DBに保存します。

念のため、sessionは休止状態空間に固有のものではなく、別の休止状態プロジェクトと同様の方法で作成されます。

ここで、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

予想どおり、FieldPointTypeGEOMETRYです。 このため、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)が正確に円上にあることです。 それでも、クエリはこのポイントを返しません。 これは、the within() function returns true only if the given Geometry is completely within another Geometryが原因です。

7.2. ST_TOUCHES()の例

ここでは、データベースにPolygonsのセットを挿入し、指定されたPolygonに隣接するPolygonsを選択する例を示します。 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に隣接するPolygonsを検索しています。 明らかに、指定されたPolygonに接触するエッジがないため、3番目のPolygonは結果に返されません。

8. 結論

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

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

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