Запросы
Чтение сущностей — фильтры, сортировка, страницы и терминальные операции.
Форма#
users, err := db.Users.Query().
Where(Users.Active.Eq(true)).
OrderBy(Users.CreatedAt.Desc()).
Limit(50).
All(ctx)Query изменяем и одноразов. Clone ответвляет копию, когда нужна база:
base := db.Users.Query().Where(Users.Active.Eq(true))
recent := base.Clone().Where(Users.CreatedAt.Gte(cutoff))
count, _ := base.Clone().Count(ctx)Терминалы#
| Метод | Возвращает |
|---|---|
All(ctx) | []E |
One(ctx) | E или ErrNotFound |
Count(ctx) | int64 |
Exists(ctx) | bool |
Rows(ctx) | iter.Seq2[E, error] — потоком |
SQL() | запрос и аргументы, без выполнения |
Ошибки построения накапливаются и приходят вместе из терминала, поэтому запрос, который нельзя построить, никогда не доходит до PostgreSQL:
_, err := db.Users.Query().Where(broken).OrderBy(alsoBroken).All(ctx)
// err сообщает про обе, а не только про первуюWhere#
Несколько вызовов Where объединяются через AND. Это частый случай, и он оставляет динамическую фильтрацию читаемой:
q := db.Users.Query()
if email != "" {
q = q.Where(Users.Email.ILike("%" + email + "%"))
}
if onlyActive {
q = q.Where(Users.Active.Eq(true))
}
users, err := q.All(ctx)Для OR — явно:
db.Users.Query().Where(orm.Or(
Users.Email.Eq("a@example.com"),
Users.Email.Eq("b@example.com"),
))orm.And() по пустому срезу даёт запрос вообще без WHERE, а не WHERE TRUE.
Сортировка и страницы#
db.Users.Query().
OrderBy(Users.CreatedAt.Desc(), Users.ID.Asc()).
Limit(20).
Offset(40)Limit(0) — законный запрос, возвращающий ничего. Ошибка — только отрицательное значение.
На больших таблицах keyset-пагинация лучше OFFSET, и типизированный API выражает её прямо:
db.Users.Query().
Where(orm.Or(
Users.CreatedAt.Lt(lastSeenAt),
orm.And(Users.CreatedAt.Eq(lastSeenAt), Users.ID.Lt(lastSeenID)),
)).
OrderBy(Users.CreatedAt.Desc(), Users.ID.Desc()).
Limit(20)Потоковое чтение#
Rows отдаёт строки по мере их прихода, поэтому большой результат никогда не существует целиком:
for user, err := range db.Users.Query().Rows(ctx) {
if err != nil {
return err
}
if err := handle(user); err != nil {
return err
}
}Связи требуют отдельного запроса, а для него нужно увидеть все строки — ровно то, чего потоковое чтение и избегает. Поэтому Rows отказывает With, а не буферизует молча.
Блокировки#
db.Users.Query().Where(Users.ID.Eq(id)).ForUpdate()
db.Users.Query().Lock(orm.ForUpdateStrong, orm.SkipLocked())
db.Users.Query().Lock(orm.ForShare, orm.NoWait())Блокировать nullable-сторону outer join PostgreSQL отказывается, поэтому при наличии джойнов блокировка явно называет корневую таблицу.
Посмотреть SQL#
sql, args, err := db.Users.Query().Where(Users.Active.Eq(true)).SQL()
// SELECT "users"."id", ... FROM "public"."users" WHERE "users"."active" = $1
// args: [true]Значений в SQL нет никогда. Каждое — параметр привязки, включая те, что внутри фрагментов Expr.
Разобранные примеры#
Три разные схемы: фильтр читается по-разному в зависимости от того, что он фильтрует.
Отслеживание посылок#
Отправления, которые вышли со склада и не пришли, — сначала самые старые: очередь, которую разбирает диспетчер.
stuck, err := db.Shipments.Query().
Where(Shipments.DepartedAt.IsNotNull()).
Where(Shipments.ArrivedAt.IsNull()).
Where(Shipments.DepartedAt.Lt(time.Now().Add(-48*time.Hour))).
OrderBy(Shipments.DepartedAt.Asc()).
Limit(100).
All(ctx)Две проверки на NULL — это и есть весь запрос: отправлено проставлено, доставлено
нет. На nullable-колонке это читается так же просто, как звучит, а на NOT NULL
ни одного из этих методов просто нет.
Бухгалтерская книга#
Последняя строка выписки по счёту — и есть ли они вообще.
latest, err := db.Entries.Query().
Where(Entries.AccountID.Eq(accountID)).
OrderBy(Entries.PostedAt.Desc(), Entries.ID.Desc()).
One(ctx)
if errors.Is(err, orm.ErrNotFound) {
// новый счёт, а не сломанный
}
any, err := db.Entries.Query().Where(Entries.AccountID.Eq(accountID)).Exists(ctx)ErrNotFound от One — нормальный ответ на нормальный вопрос. Exists выбирает
константу, а не строку, поэтому вопрос ничего не стоит на декодировании.
Таблица телеметрии#
Показания набора устройств за окно времени, потоком — их слишком много, чтобы держать целиком.
for reading, err := range db.Readings.Query().
Where(Readings.DeviceID.In(deviceIDs...)).
Where(Readings.At.Between(from, to)).
OrderBy(Readings.At.Asc()).
Rows(ctx) {
if err != nil {
return err
}
if err := accumulate(reading); err != nil {
return err
}
}Rows отдаёт строки по мере прихода. Всё окно никогда не лежит в памяти
целиком — в этом разница между отчётом, который отрабатывает, и тем, который
убивают.