GORM Фантастическая ORM для Golang.
PostGIS расширяет возможности реляционной базы данных PostgreSQL , добавляя поддержку хранения, индексирования и запросов геопространственных данных.
В этой статье поделимся своим опытом интеграции GORM и PostGIS, сложностями при попытке использования gorm для работы с геометрическими данными и конечно предлагаем готовое решение.
Изначально эта статья была опубликована здесь.
Задача
Реализация микросервиса, отвечающего за работу с геоданными:
Хранение полигонов зон доставки;
Хранение точек доставки (адресов покупателей);
Поиск вхождений точки в зоны доставки заведений;
Хранение маршрутов доставки, рассчитанных с учётом различных параметров.
Поскольку, большая часть микросервисов в проекте (часть проекта описана в кейсе Telegram App Shawarma bar & KINTO'S) написана на Go с основной реляционной СУБД PostgreSQL. Было принято решение хранить данные микросервиса также в PostgreSQL, учитывая предшествующий положительный опыт работы с его расширением PostGIS.
Был определён следующий стек технологий: Go, GORM, PostgreSQL, PostGIS.
Проблема интеграции GORM и PostGIS
Однако с самого начала было понятно что GORM не поддерживает геометрические типы данных "из коробки", поэтому было принято решение использовать сырые SQL-запросы. Это решение не позволяло раскрыть возможности GORM и значительно увеличило сложность разработки и сопровождения микросервиса.
Поиск решения в интернете не привёл к успеху. Единственное, что удалось найти - это пример реализации пользовательского типа Location на сайте GORM и несколько библиотек, поддерживающих лишь базовые геометрические типы (Point и в некоторых случаях Polygon).
Пример использования SQL-запросов для работы с геоданными
Для работы с геометрическими данными приходилось использовать SQL-запросы. Например, для получения полигона:
SELECT p.id, p.address_id, ST_AsText(p.geo_polygon) as geo_polygon, FROM public.polygons p WHERE p.id = $1
Поле geo_polygon содержит полигон, с помощью функции ST_AsText преобразуется в текстовый формат wkt.
Пример строки WKT, которая может содержаться в поле geo_polygon:
POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))
Затем этот текст нужно преобразовать в структуру для работы с полигоном внутри приложения.
Для создания таблиц с геометрическими типами данных (миграции) также приходилось писать SQL-запросы:
CREATE TABLE IF NOT EXISTS public.addresses ( id bigserial, address text NULL, geo_point geometry NOT NULL, CONSTRAINT pk_address_id PRIMARY KEY(id) );
Основные проблемы
По сравнению с функциями которые используют возможности gorm в полном объёме, функции с SQL запросами были в 2-3 раза длиннее и соответственно менее читаемые.
Пропадает возможность использовать автоматическую миграцию gorm.
Был выбран неподходящий формат данных, так как использование WKT в разы менее производителен чем WKB, убедиться в этом помог бенчмарк, который наглядно показывает разницу в производительности при работе с форматами WKT и WKB.
Результаты бенчмарка:
Format | size | convert to | convert from | serialize to parquet | deserialize from parquet |
|---|---|---|---|---|---|
wkb | 54.6 MB | 0.089s | 0.046s | 0.044s | 0.03s |
wkt | 71.6 MB | 0.44s | 0.45s | 0.38s | 0.12s |
Из результатов видно, что преобразование полигона в текстовый формат WKT для передачи в БД занимает в 5 раз больше времени, чем преобразование в бинарный формат WKB. А получения значения из базы в текстовом формате потребует в 9 раз больше времени чем данных в бинарном формате.
Решение
Для упрощения и оптимизации работы с геоданными в GORM было принято решения написать свои типы для геометрий, которые будут расширять функциональность gorm.
Реализована поддержка следующих типов:
Point
LineString
Polygon
MultiPoint
MultiLineString
MultiPolygon
GeometryCollection
Реализация интерфейсов:
sql.Scanner и driver.Valuer способствовала простому получению и записи данных.
schema.GormDataTypeInterface обеспечила правильное поведение GORM при миграции таблиц с геометрическими типами.
fmt.Stringer добавила возможность отображения данных в человекочитаемом формате WKT.
В основе решения лежит библиотека go-geom реализующая эффективные типы геометрии для геопространственных приложений, кроме того go-geom имеет поддержку неограниченного количества измерений, реализует кодирование и декодирование в формат wkb и другие форматы, функции для работы с 2D и 3D топологиями и другие особенности.
Решение является в некотором роде адаптацией go-geom для работы с GORM и получило название georm (сочетание слов "geometry" и "ORM"). Вы можете ознакомиться с решением на GitHub georm.
Примеры использования
Описание структур с геометрическими типами:
type Address struct { ID uint `gorm:"primaryKey"` Address string GeoPoint georm.Point } type Zone struct { ID uint `gorm:"primaryKey"` Title string GeoPolygon georm.Polygon }
Простая, автоматическая миграция gorm.
db.AutoMigrate( // CREATE TABLE "addresses" ("id" bigserial,"address" text,"geo_point" Geometry(Point, 4326),PRIMARY KEY ("id")) Address{}, // CREATE TABLE "zones" ("id" bigserial,"title" text,"geo_polygon" Geometry(Polygon, 4326),PRIMARY KEY ("id")) Zone{}, )
Полноценное использование возможностей ORM для запросов, передача геометрических данных в wkb формате:
// INSERT INTO "addresses" ("address","geo_point") VALUES ('some address','010100000000000000000045400000000000003840') RETURNING "id" tx.Create(&Address{ Address: "some address", GeoPoint: georm.Point{ Geom: geom.NewPoint(geom.XY).MustSetCoords(geom.Coord{42, 24}), }, }) // ... // INSERT INTO "zones" ("title","geo_polygon") VALUES ('some zone','010300000001000000050000000000000000003e4000000000000024400000000000004440000000000000444000000000000034400000000000004440000000000000244000000000000034400000000000003e400000000000002440') RETURNING "id" tx.Create(&Zone{ Title: "some zone", GeoPolygon: georm.Polygon{ Geom: geom.NewPolygon(geom.XY).MustSetCoords([][]geom.Coord{ {{30, 10}, {40, 40}, {20, 40}, {10, 20}, {30, 10}}, }), }, }) // ... // SELECT * FROM "zones" WHERE ST_Contains(geo_polygon, '0101000020e610000000000000000039400000000000003a40') ORDER BY "zones"."id" LIMIT 1 db.Model(&Zone{}). Where("ST_Contains(geo_polygon, ?)", point). First(&result) // ...
Не большой бонус - реализация интерфейса fmt.Stringer, вывод в человеко читаемом wkt формате.
// POINT (25 26) fmt.Println(georm.Point{ Geom: geom.NewPoint(geom.XY).MustSetCoords(geom.Coord{25, 26}).SetSRID(georm.SRID), }) // POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10)) fmt.Println(georm.Polygon{ Geom: geom.NewPolygon(geom.XY).MustSetCoords([][]geom.Coord{ {{30, 10}, {40, 40}, {20, 40}, {10, 20}, {30, 10}}, }), })
Для получения дополнительной информации и примеров использования посетите репозиторий georm на GitHub.
