К содержимому

Введение

Что делает эта библиотека, чего она принципиально не делает и почему разница важна.

Тезис#

Структуры — ваши. Схема — PostgreSQL. Генератор доказывает, что они совпадают.

Большинство Go-мапперов выбирают сторону. Либо структуры — источник истины, и схема порождается из них, либо наоборот. Оба направления дают код, который кто-то руками держит в согласии, как только реальность расходится.

Здесь ничего не порождается из другого. Структуры пишете вы. Схемой владеют миграции. Команда orm читает и то и другое, показывает каждое расхождение и генерирует типизированные метаданные только из отображения, которое смогла доказать.

Следствие и есть смысл: если запрос компилируется, база сможет на него ответить.

Что это даёт#

Сгенерированные дескрипторы несут параметры типов, поэтому компилятор требует того, что доказала сверка:

go
// 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. Не «должно работать»: набор тестов совместимости отказывается запускаться меньше чем на всех пяти, потому что заявленной, но ни разу не проверенной версии верить нельзя.

Куда дальше#

Разобранные примеры#

Пример того, что делает за вас компилятор, — в четырёх несвязанных схемах.

go
// Магазин. У текста есть 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)

И четыре вещи, которые не компилируются, — то же утверждение с другой стороны:

go
Products.PriceCents.ILike("%")          // у целого числа нет ILike
Products.Name.IsNull()                  // name объявлена NOT NULL
db.Accounts.Query().Where(Products.Name.Eq("x"))  // не та сущность
orm.UnionAll[Row](byEmail, byAge)       // ветви расходятся по форме