PostGIS и JPA +11




PostGIS — открытое программное обеспечение, добавляющее поддержку географических объектов в реляционную базу данных PostgreSQL.

В этой небольшой статье будет рассмотрено использование его в Java. В частности — задача нахождения географических объектов по их координатам.

PostGIS был создан еще в 2001 году. Он является хорошим бесплатным решением для хранения картографических данных в базе. Но статья не совсем о нем, а только о частном случае — удобной работе с PostGIS средствами JPA.

Зависимости


Для нашей задачи важны такие библиотеки:

  • Hibernate 5.3.7
  • hibernate-spatial — той же версии. Теоретически, можно использовать более старые. Начиная с пятой, hibernate-spatial совпадает с hibernate. Ранее: Hibernate Spatial 1.1.x для Hibernate 3.6.x, Hibernate Spatial 1.0 для Hibernate 3.2.x — 3.5.x.
  • postgresql 42.2.4. Бралась такая версия, потому что в более новых ужесточены требования к SSL. Подбирайте версию драйвера, подходящую к версии БД.

Ну и все, что вам еще потребуется для JPA — Spring или контейнер.

Диалекты


Hibernate Spatial предоставляет геометрические абстракции для работы с пространственными базами. Как и в JPA, в первом приближении нас не интересует, какая база данных используется на сервере.

Официально поддерживаются PostgresSQL, Oracle, MySQL, MS SQLServer, GeoDB (H2), DB2. Детали поддержки функций. Может показаться, что Мускул — аутсайдер. Но в 8-й версии поддержку пространственных данных прилично улучшили.

Мы используем Postgres. Но нужно указать Hibernate диалект "org.hibernate.spatial.dialect.postgis.PostgisDialect" вместо стандартного постгресовского.

Пора кодить


Таблица в PostGIS может иметь какие угодно поля. Просто стандартно одно из них будет типа geometry. А еще есть geography (не поддерживается сейчас в Hibernate). Если не научить Java работать с этим типом, он будет восприниматься как blob или String вида «01010000207B7F0000188D594CC9B22541BC4E56674F2C5541».

Конечно, можно работать с PostGis'ом на чистом JBDC. Пример. Но это требует отдельной кропотливой работы с org.postgis.PGgeometry. Это совсем не те классы, про какие будет статья. И никакой переносимости уже не будет.

Мы идем в JPA и создаем простой класс:

@Entity
public class AdressBuilding implements Serializable {
    @Id
    private Integer id;
    private Point geom;
    ...

Остальные поля опущены (географический объект может хранить любую информацию). Здесь ничего необычного — стандартный класс сущности. Интересен только объект класса Point — точка трехмерного пространства.

Тут и далее используются классы из пакета com.vividsolutions.jts.geom.

JTS стал стандартом де-факто для представления геопространственных данных. Он реализует спецификацию Simple Feature Specification / Simple Feature Access, созданную OpenGIS еще в 90е.

Уточнение. Point наследуется от абстрактного класса Geometry. Он содержит такие нестатические поля:

    protected Envelope envelope;
    protected final GeometryFactory factory;
    protected int SRID;
    private Object userData;

Envelope — минимальная ограничительная рамка для этой геометрии. Но она может возвращаться в виде геометрии. И тогда у вас будет бесконечная попытка сериализации.

SRID — номер системы координат. Их существует очень много. Основные различия: формат координат (метры, градусы...), точка отсчета и форма Земли (Земля не круглая). PostGis знает много систем координат и умеет их преобразовывать.

Как я уже сказал, в базе у нас тип geometry. Я же сразу использовал конкретный класс Point для удобства, ведь в этой таблице у меня только точечные объекты. Но PostGIS теоретические может хранить несколько типов геометрии сразу. Просто в каждой геометрии указан ее тип:

"geometry":{"type":"MultiPolygon","coordinates":...

Если верить StackOverflow, использование нескольких геометрий в одной таблице замедляет запросы. Геометрии могут быть и вложенными. Типы:


Запросы к БД


С реализацией класса разобрались. теперь пора получить их из базы. Наши точки — дома, точнее их адреса. Вы можете делать привычные SQL запросы: получать домики по id, номеру, количеству бабушек…

Нас же сейчас интересуют пространственные запросы. Например, найти дом по координатам. Пусть искомые координаты x,y, а +-delta — желаемая область поиска. Основные запросы в STS выполняются на соотношение геометрий. Поэтому нам нужно ее создать:

        Coordinate c1 = new Coordinate(x - delta, y - delta);
...
        Coordinate[] coordinates = new Coordinate[]{c1, c2, c3, c4, c1};

        GeometryFactory GEOMETRY_FACTORY = new GeometryFactory();//static заранее
        Polygon square_window = GEOMETRY_FACTORY.createPolygon(coordinates);
        square_window.setSRID(32635);

Если мы не укажем систему координат, PostGis откажется их сравнивать. Вы или знаете код вашей системы, или получите его из любой точки кодом .getGeom().getSRID().

Дальше посылаем запрос вида:

    "select a "
    + "from AdressBuilding a "
    + "where within(a.geom, :window) = true"

Запрос within означает проверку, находится ли геометрия внутри другой. Не пугайтесь, если ваша IDE скажет, что в JPA не может быть таких запросов. Hibernate Spatial преобразует его в:
 where
        st_within(adressbuil0_.geom, ?)=true

Где st_within — уже функция PostGis.

Есть еще несколько вариантов, как получить тот же результат — точка попала в квадрат. contains(:window, a.geom) / intersects(a.geom, :window)...



Детальное описание спецификаций здесь.

Послесловие


Точки мы получили — делайте теперь с ними что пожелаете.

Я тестировал случай небольшой базы на сервере с относительно большим объемом оперативы. Если грузить по максимуму и забыть про индексы, задача поиска упрется в процессор.
В Postgres есть много разных индексов. И часть из них помогают Постгису. Исследование показало, что для точек подходит только GIST?
CREATE INDEX [indexname] ON [tablename] USING GIST ( [geometryfield] );

Но чаще всего, когда вы импортируете данные в PostGis, индексы создаются автоматом…

Уточнения и дополнительная информация приветствуется.

Использованный мануал:

Для Hibernate 5
Для Hibernate 4




К сожалению, не доступен сервер mySQL