- PVSM.RU - https://www.pvsm.ru -
GORM [1] Фантастическая ORM для Golang.
PostGIS [2] расширяет возможности реляционной базы данных PostgreSQL [3] , добавляя поддержку хранения, индексирования и запросов геопространственных данных.
В этой статье поделимся своим опытом интеграции GORM и PostGIS, сложностями при попытке использования gorm для работы с геометрическими данными и конечно предлагаем готовое решение.
Изначально эта статья была опубликована здесь [4].
Реализация микросервиса, отвечающего за работу с геоданными:
Хранение полигонов зон доставки;
Хранение точек доставки (адресов покупателей);
Поиск вхождений точки в зоны доставки заведений;
Хранение маршрутов доставки, рассчитанных с учётом различных параметров.
Поскольку, большая часть микросервисов в проекте (часть проекта описана в кейсе Telegram App Shawarma bar & KINTO'S [5]) написана на Go с основной реляционной СУБД PostgreSQL. Было принято решение хранить данные микросервиса также в PostgreSQL, учитывая предшествующий положительный опыт работы с его расширением PostGIS.
Был определён следующий стек технологий: Go, GORM, PostgreSQL, PostGIS.
Однако с самого начала было понятно что GORM не поддерживает геометрические типы данных "из коробки", поэтому было принято решение использовать сырые SQL-запросы. Это решение не позволяло раскрыть возможности GORM и значительно увеличило сложность разработки и сопровождения микросервиса.
Поиск решения в интернете не привёл к успеху. Единственное, что удалось найти - это пример реализации пользовательского типа Location на сайте GORM [6] и несколько библиотек, поддерживающих лишь базовые геометрические типы (Point и в некоторых случаях Polygon).
Для работы с геометрическими данными приходилось использовать 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, убедиться в этом помог бенчмарк [7], который наглядно показывает разницу в производительности при работе с форматами 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 [8] и driver.Valuer [9] способствовала простому получению и записи данных.
schema.GormDataTypeInterface [10] обеспечила правильное поведение GORM при миграции таблиц с геометрическими типами.
fmt.Stringer [11] добавила возможность отображения данных в человекочитаемом формате WKT.
В основе решения лежит библиотека go-geom [12] реализующая эффективные типы геометрии для геопространственных приложений, кроме того go-geom имеет поддержку неограниченного количества измерений, реализует кодирование и декодирование в формат wkb и другие форматы, функции для работы с 2D и 3D топологиями и другие особенности.
Решение является в некотором роде адаптацией go-geom для работы с GORM и получило название georm (сочетание слов "geometry" и "ORM"). Вы можете ознакомиться с решением на GitHub georm [13].
Описание структур с геометрическими типами:
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 [13].
Автор: utherbit
Источник [14]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/golang/398032
Ссылки в тексте:
[1] GORM: https://gorm.io/
[2] PostGIS: https://postgis.net/
[3] PostgreSQL: https://postgresql.org/
[4] здесь: https://ybru.ru/blog/kak-podruzhit-gorm-i-post-gis-reshenie-promyshlennogo-urovnya/
[5] Telegram App Shawarma bar & KINTO'S: https://ybru.ru/projects/telegram-app-shawarma-bar-and-kinto-s/
[6] сайте GORM: https://gorm.io/docs/data_types.html
[7] бенчмарк: https://github.com/opengeospatial/geoparquet/discussions/170#discussioncomment-5800433
[8] sql.Scanner: https://pkg.go.dev/database/sql#Scanner
[9] driver.Valuer: https://pkg.go.dev/database/sql/driver#Valuer
[10] schema.GormDataTypeInterface: https://pkg.go.dev/gorm.io/gorm/schema#GormDataTypeInterface
[11] fmt.Stringer: https://pkg.go.dev/fmt#Stringer
[12] go-geom: https://github.com/twpayne/go-geom
[13] georm: https://github.com/ybru-tech/georm
[14] Источник: https://habr.com/ru/articles/847048/?utm_source=habrahabr&utm_medium=rss&utm_campaign=847048
Нажмите здесь для печати.