Введение
Что делает эта библиотека, чего она принципиально не делает и почему разница важна.
Тезис#
Структуры — ваши. Схема — PostgreSQL. Генератор доказывает, что они совпадают.
Большинство Go-мапперов выбирают сторону. Либо структуры — источник истины, и схема порождается из них, либо наоборот. Оба направления дают код, который кто-то руками держит в согласии, как только реальность расходится.
Здесь ничего не порождается из другого. Структуры пишете вы. Схемой владеют миграции. Команда orm читает и то и другое, показывает каждое расхождение и генерирует типизированные метаданные только из отображения, которое смогла доказать.
Следствие и есть смысл: если запрос компилируется, база сможет на него ответить.
Что это даёт#
Сгенерированные дескрипторы несут параметры типов, поэтому компилятор требует того, что доказала сверка:
// Predicate[User] не дотянется до запроса по Post.
db.Orders.Query().Where(Users.Email.Eq("a@example.com")) // не компилируется
// У текстовой колонки есть Like. У целочисленной нет.
Users.Email.ILike("%@example.com") // нормально
Users.Age.ILike("%") // не компилируется
// У NOT NULL колонки нет IsNull.
Users.Bio.IsNull() // нормально: bio nullable
Users.Email.IsNull() // не компилируетсяЭто не соглашение об именовании и не правило линтера. Это следует из типа дескриптора, который пришёл из каталога.
Чего она не делает#
Инструмент определяется и тем, чего он делать не станет:
- Нет ленивой загрузки. Связь загружается потому, что вы её попросили. Цикл по срезу не превратится незаметно в запрос на строку.
- Нет отслеживания изменений, identity map и
Save.Insert,UpdateиDeleteвыражают намерение. Ничего не додумывается. - Нет неявной обработки нулевых значений. Структура с
Active: falseсохранитfalse, потому что библиотека не отличит это от поля, которого никто не касался. Попросить значение по умолчанию — этоorm.Default, и это отдельное явное действие. - Нет сборки SQL из строк. Любое значение становится параметром привязки.
ExprиRawпринимают текст SQL намеренно; ни один не принимает значения, вставленные в него. - Нет случайных
UPDATEбезWHERE. Обновление или удаление без условий отвергается сErrMissingWhere, если толькоAllне сказал, что имелись в виду все строки.
Откуда берутся гарантии#
Три слоя, у каждого своя работа.
| Слой | Владеет | Доказывает |
|---|---|---|
| Миграции | Схемой | Что базу можно собрать с нуля |
| Сверка | Отображением | Что у каждого поля есть колонка совместимого типа |
| Сгенерированный код | Дескрипторами | Что неверный запрос не компилируется |
Если сверка не может доказать поле, дескриптор для него не появится — будет ошибка с именем поля, колонки и способом починки. Отката к any нет.
Поддерживаемые версии PostgreSQL#
14, 15, 16, 17 и 18. Не «должно работать»: набор тестов совместимости отказывается запускаться меньше чем на всех пяти, потому что заявленной, но ни разу не проверенной версии верить нельзя.
Куда дальше#
- Установка — модуль и CLI.
- Быстрый старт — схема, структура и запрос за несколько минут.
- Ключевые идеи — словарь, которым пользуется остальная документация.
Разобранные примеры#
Пример того, что делает за вас компилятор, — в четырёх несвязанных схемах.
// Магазин. У текста есть Like, и компилятор это знает, потому что так сказал каталог.
db.Products.Query().Where(Products.Name.ILike("%lamp%"))
// Бухгалтерия. Проверка баланса в WHERE, поэтому овердрафт — это обновление,
// не нашедшее строк, а не гонка.
db.Accounts.Update().
Set(Accounts.Balance.SetExpr(Accounts.Balance.Sub(amount))).
Where(Accounts.ID.Eq(id)).
Where(Accounts.Balance.Gte(amount)).
Exec(ctx)
// Календарь. Пересечение — один предикат, а не четыре сравнения, которые надо не перепутать.
db.Bookings.Query().Where(Bookings.During.Overlaps(orm.ClosedOpen(from, to)))
// Автопарк. Три уровня за три запроса, сколько бы ни было строк.
db.Depots.Query().With(Depots.Vehicles.With(Vehicles.Services)).All(ctx)И четыре вещи, которые не компилируются, — то же утверждение с другой стороны:
Products.PriceCents.ILike("%") // у целого числа нет ILike
Products.Name.IsNull() // name объявлена NOT NULL
db.Accounts.Query().Where(Products.Name.Eq("x")) // не та сущность
orm.UnionAll[Row](byEmail, byAge) // ветви расходятся по форме