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.
gohrytt
НЕ
ИСПОЛЬЗОВАТЬ
GORM
(как и любые другие магические концепции, польза от применения которых сомнительна а вред явно ощутим)
utherbit Автор
Gorm имеет достаточно большое сообщество, эта статья написана что бы помочь разработчиками, которые работают с Gorm и столкнулись с той же проблемой, что и мы.
Я просто делюсь готовым решением, в надежде что это кому то поможет)
filippov70
а тем более для таких сложных типов как geometry и geography
utherbit Автор
GORM будет просто вызывать методы Scan и Value, которые описаны в стандартной библиотеке database/sql. Не какой магии в чтении и записи нет, поэтому и проблем быть не может.
В случае более сложных запросов, например, когда Вам нужно найти полигоны в которые входит точка
как в примере в статье, можно считать что GORM выполняет роль SQL билдера, в этом тоже я думаю ничего магического нет.WhiskyBar
Я пишу на Питоне, но немного знаком с Go. Часто встречаю подобную точку зрения «В Go ORM не нужен». Но не встречал подробного разбора, почему. Можете объяснить почему?
gohrytt
Есть две с половиной основные причины:
Производительность - в го совершенно невозможно сделать производительную ORM - в языке нет comptime, нет аннотаций, нет ничего из того, что нужно чтобы ORM производила все расчёты при компиляции, или хотя бы на старте приложения и не грузила рантайм.
Магия - в го у нас принято, что явное лучше неявного. Сама идея ORM - добавить магии, "ты просто пишешь некую entity, а дальше всё само". Этот подход может быть не так плох для проектов, которые не требуют оптимизации, но это противоречит явности, плюс исторически сложилось, что го берут как раз там, где другие простые языки показали себя медленными и требуется увеличение скорости.
Резюмируя - вы нарушаете один из основных принципов и теряете в производительности, ради сомнительных преимуществ.
Лично я например слабо понимаю чем условное
db.Find(&entities).Error
лучше чем
db.Query("select * from entities", &entities)