«1. Введение

В этой статье мы рассмотрим пространственное расширение Hibernate, hibernate-spatial.

Начиная с версии 5, Hibernate Spatial предоставляет стандартный интерфейс для работы с географическими данными.

2. Общие сведения о Hibernate Spatial

Географические данные включают в себя представление таких объектов, как точка, линия, многоугольник. Такие типы данных не являются частью спецификации JDBC, поэтому JTS (JTS Topology Suite) стал стандартом для представления пространственных типов данных.

Помимо JTS, пространственный Hibernate также поддерживает Geolatte-geom — недавнюю библиотеку, в которой есть некоторые функции, недоступные в JTS.

Обе библиотеки уже включены в проект hibernate-spatial. Использование одной библиотеки над другой — это просто вопрос того, из какой банки мы импортируем типы данных.

Хотя Hibernate пространственная поддержка различных баз данных, таких как Oracle, MySQL, PostgreSQLql/PostGIS и некоторые другие, поддержка конкретных функций базы данных неодинакова.

Лучше обратиться к последней документации Hibernate, чтобы проверить список функций, для которых hibernate поддерживает данную базу данных.

В этой статье мы будем использовать в памяти Mariadb4j, который поддерживает полную функциональность MySQL.

Конфигурация для Mariadb4j и MySql аналогична, даже библиотека mysql-connector работает для обеих этих баз данных.

3. Зависимости Maven

Давайте посмотрим на зависимости Maven, необходимые для настройки простого проекта hibernate-spatial:

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

Первым шагом является создание hibernate.properties в каталоге ресурсов:

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

Единственное, что специфично для hibernate-spatial, это диалект MySQL56SpatialDialect. Этот диалект расширяет диалект MySQL55Dialect и предоставляет дополнительную функциональность, связанную с типами пространственных данных.

Код, специфичный для загрузки файла свойств, создания SessionFactory и создания экземпляра Mariadb4j, такой же, как и в стандартном проекте гибернации.

5. Понимание типа Geometry

Geometry — это базовый тип для всех пространственных типов в JTS. Это означает, что другие типы, такие как Point, Polygon и другие, являются производными от Geometry. Тип Geometry в java также соответствует типу GEOMETRY в MySql.

Анализируя строковое представление типа, мы получаем экземпляр Geometry. Вспомогательный класс WKTReader, предоставляемый JTS, можно использовать для преобразования любого известного текстового представления в тип 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;

    // standard getters and setters
}

~~ ~ Обратите внимание, что объект PointEntity содержит пространственный тип Point. Как показано ранее, точка представлена ​​двумя координатами:

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

Метод insertPoint() принимает общеизвестное текстовое (WKT) представление точки, преобразует его в экземпляр точки и сохраняет в базе данных.

Напоминаем, что сеанс не является специфичным для 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);
}

Вызов toString() для точки возвращает WKT-представление точки. Это связано с тем, что класс Geometry переопределяет метод toString() и внутренне использует WKTWriter, дополнительный класс для WKTReader, который мы видели ранее.

Как только мы запустим этот тест, hibernate создаст для нас таблицу PointEntity.

«Давайте посмотрим на эту таблицу:

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

Как и ожидалось, тип точки поля — ГЕОМЕТРИЯ. Из-за этого при извлечении данных с помощью нашего редактора SQL (например, рабочей среды MySql) нам нужно преобразовать этот тип GEOMETRY в удобочитаемый текст:

select id, astext(point) from PointEntity;

id      astext(point)
1       POINT(2 4)

Однако, поскольку hibernate уже возвращает представление WKT, когда мы вызываем toString( ) в Geometry или любом из его подклассов, нам не нужно беспокоиться об этом преобразовании.

7. Использование пространственных функций

7.1. Пример ST_WITHIN()

Теперь мы рассмотрим использование функций базы данных, которые работают с пространственными типами данных.

Одной из таких функций в MySQL является 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(). Радиус удваивается перед вызовом метода 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 сопоставляет свою функцию inside() с функцией ST_WITHIN() MySql.

Интересное наблюдение здесь состоит в том, что точка (3, 4) попадает точно на окружность. Тем не менее, запрос не возвращает эту точку. Это связано с тем, что функция inside() возвращает значение true только в том случае, если данная геометрия полностью находится внутри другой геометрии.

7.2. ST_TOUCHES() Пример

Здесь мы представим пример, который вставляет набор полигонов в базу данных и выбирает полигоны, смежные с заданным полигоном. Давайте быстро взглянем на класс 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() для поиска полигонов, смежных с заданным полигоном. Ясно, что третий многоугольник не возвращается в результате, поскольку ребро не касается данного многоугольника.

8. Заключение

В этой статье мы увидели, что hibernate-spatial значительно упрощает работу с пространственными типами данных, так как заботится о низкоуровневых деталях.

Несмотря на то, что в этой статье используется Mariadb4j, мы можем заменить его MySql без изменения конфигурации.

Как всегда, полный исходный код этой статьи можно найти на GitHub.