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 . Понимание типа геометрии
Геометрия
является базовым типом для всех пространственных типов в 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
и как получить Point
из String
, давайте посмотрим на 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) представление Point
, преобразует его в экземпляр Point
и сохраняет в БД.
Напоминаем, что сеанс
не является специфичным для 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()
для Point
возвращает WKT-представление Point
. Это связано с тем, что класс Geometry
переопределяет метод toString()
и внутренне использует WKTWriter,
дополнительный класс для WKTReader
, который мы видели ранее.
Как только мы запустим этот тест, hibernate создаст для нас таблицу PointEntity
.
Давайте посмотрим на эту таблицу:
desc PointEntity;
Field Type Null Key
id bigint(20) NO PRI
point geometry YES
Как и ожидалось , тип
точки поля
— GEOMETRY
.
Из-за этого при извлечении данных с помощью нашего редактора 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()
Пример
Здесь мы представим пример, который вставляет набор Polygon
s в базу данных и выбирает Polygon
s, смежные с данным Polygon
. Давайте быстро взглянем на класс 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()
, чтобы найти Polygon
, смежный с заданным Polygon
. Очевидно, что третий Polygon
не возвращается в результате, так как ребро не касается данного Polygon
.
8. Заключение
В этой статье мы увидели, что hibernate-spatial значительно упрощает работу с пространственными типами данных, поскольку заботится о низкоуровневых деталях.
Несмотря на то, что в этой статье используется Mariadb4j, мы можем заменить его на MySql без изменения какой-либо конфигурации.
Как всегда, полный исходный код этой статьи можно найти на GitHub .