PostGIS
Пространственные типы, которые остаются пространственными: geometry и geography порознь.
Подключается отдельно#
Поддержка PostGIS — отдельный пакет. Проект, который его не импортирует, никогда не увидит пространственного API, а корневая библиотека ничего не знает о геометрии:
import "github.com/AlexAli29/orm/postgis"Всё в нём собирается через ту единственную границу расширения, которую открывает
корневой пакет. Нет ни второго компилятора запросов, ни второй модели выражений:
пространственный предикат — это такой же orm.Predicate, и он вкладывается в
составные запросы, CTE и производные таблицы без изменений.
Два различения, которые никогда не смешиваются#
geometry — декартова, в тех единицах, которые задаёт система координат SRID. geography — на сфероиде, расстояния и длины в метрах.
Это разные типы PostgreSQL с разным поведением индексов и разными ответами, поэтому здесь это разные типы Go. Преобразование между ними — то, что вы пишете сами, а не то, что случается с вами.
И два факта путешествуют вместе с каждым значением и каждой колонкой:
- форма — Point, LineString, Polygon и их множественные варианты;
- SRID — в какой системе координат эти числа.
Потеря любого из них — это способ получить запрос, сравнивающий метры с градусами и возвращающий число.
Объявление пространственной колонки#
Тег pgtype несёт форму и систему координат, потому что из Go-типа не выводится
ни то, ни другое:
//orm:table public.places
type Place struct {
ID int64 `orm:"pk,identity"`
Name string
// На сфероиде. Расстояния приходят в метрах.
Spot postgis.Geography `orm:"pgtype:geography(Point,4326)"`
// Декартова, в градусах WGS 84.
Location postgis.Geometry `orm:"pgtype:geometry(Point,4326)"`
// То же место в web Mercator. Соотнести его с Location без преобразования —
// ошибка, которую SRID делает видимой.
Projected *postgis.Geometry `orm:"pgtype:geometry(Point,3857)"`
Footprint *postgis.Geometry `orm:"pgtype:geometry(Polygon,4326)"`
}Указатель — nullable-колонка, как и везде. Генератор выдаёт GeomCol, GeogCol
и их nullable-формы, каждая несёт объявленные SRID, вид и размерность.
Запросы#
postgis.Of поднимает колонку geometry в пространственное выражение,
postgis.OfGeog — то же для geography:
// Всё в пределах 5 км от точки, на сфероиде — в метрах, потому что geography
// измеряет в метрах.
here := postgis.GeographyPoint(-0.1276, 51.5072)
places, err := db.Places.Query().
Where(postgis.OfGeog(Places.Spot).
DWithin(postgis.GeogValue[Place](here), 5000)).
All(ctx)// Декартовы отношения, на geometry.
db.Places.Query().Where(postgis.Of(Places.Location).Intersects(v))
db.Places.Query().Where(postgis.Of(Places.Location).Within(v))
db.Places.Query().Where(postgis.Of(Places.Location).Contains(v))Операторы по ограничивающей рамке названы именно так#
postgis.Of(Places.Location).BBoxIntersects(v) // &&
postgis.Of(Places.Location).BBoxContains(v) // ~
postgis.Of(Places.Location).BBoxWithin(v) // @&& — это не ST_Intersects. Он сравнивает ограничивающие рамки: это дешевле и
отвечает на другой вопрос, поэтому у него другое имя, а не вид «быстрой версии»
точного оператора.
Измерения и преобразования#
Они возвращают обычный orm.Value, поэтому годятся в проекции и сортировки как
всё остальное:
distance := postgis.OfGeog(Places.Spot).Distance(postgis.GeogValue[Place](here))
type Near struct {
Name string
Metres float64
}
var near = orm.Project2(
Places.Name, distance,
func(name string, m float64) Near { return Near{name, m} },
)
rows, err := orm.Select(db.Places, near).
OrderBy(distance.Asc()).
Limit(20).
All(ctx)На выражении доступны также Area, Length, Centroid, Buffer, Boundary,
Azimuth, AsText, AsEWKT, AsGeoJSON, AsBinary, AsEWKB и
AsGeography — то самое преобразование, которое вы делаете осознанно. У каждого
есть форма …Null для nullable-колонки, потому что измерение NULL-геометрии —
это NULL.
Агрегаты#
postgis.Collect(g) // ST_Collect -> *Geometry
postgis.UnionAgg(g) // ST_Union -> *Geometry
postgis.Extent(g) // ST_Extent -> *Box2D
postgis.Extent3D(g) // ST_3DExtent -> *Box3DРегистрация типов#
pgx нужно рассказать о типах PostGIS на каждом соединении:
cfg.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error {
return postgis.Register(ctx, conn)
}RegisterIfPresent — терпимая форма: она сообщает, было ли расширение, вместо
того чтобы падать. Это то, что нужно бинарнику, который работает и с
пространственными базами, и с обычными.
На каких версиях это доказано#
PostgreSQL 17 с PostGIS 3.5, 16 с 3.4 и 14 с 3.4.
Пространственный набор тестов пропускается, когда расширения нет, — правильно на
машине разработчика и неправильно в CI, поэтому CI ставит
ORM_REQUIRE_POSTGIS=1, что превращает пропуск в падение. Заявленной поддержке,
которую ничто не проверяет, верить нельзя.
Библиотека никогда не создаёт расширение. CREATE EXTENSION postgis —
привилегированная операция того, кто владеет базой.
Разобранные примеры#
Магазины рядом#
here := postgis.GeographyPoint(lon, lat)
type Near struct {
Name string
Metres float64
}
distance := postgis.OfGeog(Shops.Spot).Distance(postgis.GeogValue[Shop](here))
var near = orm.Project2(
Shops.Name, distance,
func(name string, m float64) Near { return Near{name, m} },
)
rows, err := orm.Select(db.Shops, near).
Where(postgis.OfGeog(Shops.Spot).DWithin(postgis.GeogValue[Shop](here), 2000)).
OrderBy(distance.Asc()).
Limit(10).
All(ctx)Важно, что DWithin идёт до Distance: первый может воспользоваться
пространственным индексом, второй — нет. Сначала отфильтровать, потом
отсортировать — это разница между запросом и полным сканированием.
Какая зона доставки покрывает адрес#
zone, err := db.Zones.Query().
Where(postgis.Of(Zones.Area).Contains(postgis.Of(Addresses.Point))).
One(ctx)Прямоугольник видимой области карты#
box := postgis.MakeEnvelope(west, south, east, north, 4326)
pins, err := db.Pins.Query().
Where(postgis.Of(Pins.Location).BBoxIntersects(box)).
Limit(500).
All(ctx)BBoxIntersects — это &&, сравнение ограничивающих рамок. Для прямоугольной
области это ровно тот вопрос, который нужен, и он дешёвый.
Выгрузка для картового клиента#
var geo = orm.Project2(
Zones.Name, postgis.Of(Zones.Area).AsGeoJSON(),
func(name, geom string) Feature { return Feature{name, geom} },
)Регистрация типов#
cfg.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error {
return postgis.Register(ctx, conn)
}