Hibernate Spatialの紹介

1前書き

この記事では、Hibernateの空間的拡張について調べます。http://www.hibernatespatial.org[hibernate-spatial]。

バージョン5から、Hibernate Spatialは地理データを扱うための標準的なインターフェースを提供します。

2. Hibernate Spatialの背景

地理データには、Point、Line、Polygonなどのエンティティの表現が含まれます。このようなデータ型はJDBC仕様の一部ではないため、http://www.tsusiatsoftware.net/jts/main.html[JTS(JTS Topology Suite)]が空間データ型を表すための標準になりました。

JTSとは別に、Hibernate spatialはhttps://github.com/GeoLatte/geolatte-geom[Geolatte-geom]もサポートしています - これはJTSでは利用できない機能がいくつかある最近のライブラリです。

両方のライブラリーは既にHibernate-spatialプロジェクトに含まれています。

あるライブラリを他のライブラリよりも使用するのは、どのjarからデータ型をインポートするのかという問題です。

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

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

この記事では、インメモリhttps://github.com/vorburger/MariaDB4j[Mariadb4j]を使用します。これはMySQLの全機能を維持します。

Mariadb4jとMySqlの設定は似ていますが、mysql-connectorライブラリでもこれら両方のデータベースで動作します。

3 Mavenの依存関係

単純な休止状態空間プロジェクトを設定するために必要なMavenの依存関係を見てみましょう。

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-entitymanager</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-entitymanager の最新バージョンhttps://search.maven.org/classic/#search%7Cgav%7C1%7Cg%3A%22org.hibernate%22%20AND%20a%3A%22hibernate-spatial%22[hibernate-spatial]、https://search.maven.org/classic/#search%7Cgav%7C1%7Cg%3A%22mysql%22%20AND%20a%3A%22mysql-connector-java%22[mysql-connector-java]、およびhttps://検索.maven.org/classic/#search%7Cgav%7C1%7Cg%3A%22ch.vorburger.mariaDB4j%22%20AND%20a%3A%22mariaDB4j%22[mariaDB4j]はMaven Centralから入手できます。

4. Hibernate Spatialの設定

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

hibernate.dialect=org.hibernate.spatial.dialect.mysql.MySQL56SpatialDialect//...
  • hibernate-spatialに特有のものは MySQL56SpatialDialect 方言** だけです。この方言は MySQL55Dialect 方言を拡張し、空間データ型に関連する追加機能を提供します。

プロパティファイルのロード、 SessionFactory の作成、Mariadb4jインスタンスのインスタンス化に固有のコードは、標準のHibernateプロジェクトと同じです。

5 Geometry タイプについて

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

型の String 表現を解析することによって、 Geometry のインスタンスを取得します。 JTSが提供するユーティリティクラス WKTReader は、https://en.wikipedia.org/wiki/Well-known text[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() methodが 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 は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

予想通り、 Field Point Type GEOMETRY__です。このため、(MySqlワークベンチのような)SQLエディタを使ってデータを取得している間、この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つは ST WITHIN() で、これは1つの 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() メソッドで指定された有限個の点集合で表されます。 setius() メソッドを呼び出す前に radius を2倍して、中心の周りに両方向に円を描く必要があります。

それでは先に進み、与えられた半径内で点を取得する方法を見てみましょう:

@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()__関数にマップします。

ここでの興味深い観察は、Point(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 と異なるのは、 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番目の Polygon は与えられた Polygon__に接触する辺がないので結果には返されません。

8.まとめ

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

この記事ではMariadb4jを使用していますが、設定を変更することなくMySqlに置き換えることができます。

いつものように、この記事の完全なソースコードはhttps://github.com/eugenp/tutorials/tree/master/persistence-modules/hibernate5[GitHubに載せて]をご覧ください。