Ключевые идеи
Словарь, которым пользуется остальная документация.
Сущность (entity)#
Go-структура с директивой //orm:table. Её пишете вы; ничто её не генерирует.
Дескриптор#
Сгенерированная типизированная ручка колонки: Users.Email, Orders.Placed. Её Go-тип кодирует то, что сказал каталог, — тип значения, nullable ли она и какие сравнения PostgreSQL для неё определяет.
Users.Email // orm.TextCol[User] — есть Like, ILike
Users.ID // orm.OrdCol[User, int64] — есть Gt, Between, Asc
Users.Bio // orm.NullTextCol[User] — есть IsNull
Users.Tags // orm.Col[User, []string] — только равенствоДескрипторы доступны только для чтения и безопасны для совместного использования. Всё, что похоже на мутацию — алиас таблицы, настройка связи, — возвращает копию.
Capability#
Что тип колонки умеет в SQL, а не что с ним умеет Go. uuid упорядочен, потому что его упорядочивает PostgreSQL; jsonb — нет, потому что сравнение двух jsonb-документов не отвечает ни на чей вопрос. Поэтому порядок не выводится из Go-шного cmp.Ordered.
Источник (source)#
Одно вхождение отношения в запрос. Алиас таблицы порождает второй источник, и дескриптор, построенный от одного, нельзя применить к другому — именно это делает self-join безопасным.
Repo#
Repo[E] связывает сгенерированные метаданные с исполнителем: *pgxpool.Pool, *pgx.Conn или pgx.Tx. Сгенерированный код даёт структуру DB, где по одному на сущность.
Query, SelectQuery, ComposedQuery#
Три строителя, три задачи:
Query[E]читает целые сущности из одной таблицы.SelectQuery[E, R]читает проекцию — выбранную форму результата — из одной таблицы.ComposedQuery[R]читает проекцию из источников, которые вы собрали сами: джойны, CTE, производные таблицы.
Все трое изменяемы, одноразовы и не потокобезопасны. Clone ответвляет копию.
Проекция#
Две вещи вместе: какие выражения выбрать и функция, превращающая эти
значения в ваш тип результата. Project2 принимает два выражения, поэтому его
функция принимает два параметра — в том же порядке и с теми типами, которые
имеют эти колонки.
type Summary struct {
ID int64
Email string
}
var Summaries = orm.Project2(
Users.ID, // 1-е выражение → 1-й параметр, int64, потому что id — bigint
Users.Email, // 2-е выражение → 2-й параметр, string, потому что email — text
func(id int64, email string) Summary { return Summary{ID: id, Email: email} },
)
rows, _ := orm.Select(db.Users, Summaries).All(ctx)
// []Summary — SELECT id, email FROM usersЭто значение, а не запрос: постройте одну и используйте из многих запросов. Подробно — в разделе Проекции.
Nullability и откуда она берётся#
Значение становится nullable по двум разным причинам:
- Колонка nullable.
Users.Bio— этоNullTextCol. - Запрос делает её такой. Колонка
NOT NULL, прочитанная черезLEFT JOIN, может быть NULL для строки без совпадения.
Второе — nullability, наведённая источником. Ради неё существует orm.Opt, и поэтому список выборки, читающий outer-joined источник через orm.Of, отвергается.
Ошибки#
Ошибки-сентинелы (ErrNotFound, ErrMissingWhere) оборачиваются, а не подменяются, поэтому errors.Is работает сквозь контекст каждого слоя, а собственный *pgconn.PgError остаётся доступен через errors.As. Ни один экспортированный API не паникует из-за плохого запроса, ошибки базы или неудачного сканирования.
Разобранные примеры#
Как читать тип дескриптора#
Тип и есть документация. Когда непонятно, что умеет колонка, объявление отвечает:
Products.Name // orm.TextCol[Product] — Like, ILike
Products.PriceCents // orm.OrdCol[Product, int32] — Gt, Between, Asc
Products.Discount // orm.NullOrdCol[Product, int32] — то же плюс IsNull
Products.Tags // orm.Col[Product, []string] — только равенство
Products.Meta // orm.Col[Product, map[string]any] — только равенствоМетод, который вы ожидали и не нашли, — обычно ответ «PostgreSQL не определяет этого для такого типа».
Один источник или два#
managers := Employees.As("mgr")
orm.Compose(pool, shape).
From(Employees.Source()).
LeftJoin(managers.Source(), orm.Eq(managers.ID, Employees.ManagerID))Employees.ID и managers.ID — одна и та же колонка двух разных вхождений, и
компилятор не позволит подменить одно другим. Именно это делает self-join
безопасным, а не упражнением в именовании.
Три состояния связи#
p, _ := db.Products.Query().Where(Products.ID.Eq(id)).One(ctx)
reviews, ok := p.Reviews.Get()
switch {
case !ok:
// не загружено — никто не просил
case len(reviews) == 0:
// загружено, и их действительно нет
default:
// загружено, и вот они
}Первый случай другие библиотеки схлопывают во второй — так «отзывов нет» оказывается на странице, которая отзывов не запрашивала.