Связи
Загружается то, что попросили, за предсказуемое число запросов.
Объявление#
//orm:table public.users
type User struct {
ID int64 `orm:"pk,identity"`
Orders orm.Many[Order]
}
//orm:table public.orders
type Order struct {
ID int64 `orm:"pk,identity"`
UserID int64
User orm.One[User] `orm:"fk:user_id"`
}Внешний ключ называется на той стороне, которая его держит. Генератор проверяет, что он существует и указывает туда, куда вы сказали.
Загрузка#
users, err := db.Users.Query().With(Users.Orders).All(ctx)With загружает то, что ему дали, и ничего больше. Ленивой загрузки нет, поэтому цикл по результату не превратится в запрос на строку.
Предсказуемое число запросов#
Загрузка идёт в ширину и пакетами. Число запросов зависит от формы запрошенного дерева, а не от количества строк:
db.Users.Query().
With(Users.Orders.With(Orders.Items)).
All(ctx)
// три запроса: пользователи, затем все их заказы, затем все позицииДесять пользователей или десять тысяч — их три.
Настройка связи#
Rel несёт опции, и они применяются к каждому родителю — именно поэтому «пять последних заказов каждого пользователя» это один запрос, а не N:
db.Users.Query().
With(Users.Orders.
Where(Orders.Status.Eq("paid")).
OrderBy(Orders.Placed.Desc()).
Limit(5)).
All(ctx)Фильтрация по связи без загрузки#
// пользователи хотя бы с одним оплаченным заказом
db.Users.Query().Where(Users.Orders.Any(Orders.Status.Eq("paid")))
// и без единого
db.Users.Query().Where(Users.Orders.None(Orders.Status.Eq("refunded")))Компилируются в полусоединения. Ничего не загружают, поэтому и читать нечего.
Чтение результата#
for _, u := range users {
orders, ok := u.Orders.Get()
if !ok {
// не загружено — это не то же самое, что «загружено и пусто»
continue
}
fmt.Println(len(orders))
}Три различимых состояния: не загружено, загружено и пусто, загружено и есть. Нулевое значение — «не загружено», поэтому литерал без связи говорит «я не просил», а не «там пусто».
Что определяет связанность#
PostgreSQL. Строки связываются по тому, что база считает равными ключами, поэтому citext, numeric, домены и составные ключи ведут себя как в базе, а не как Go-шное равенство.
Разобранные примеры#
Программа конференции#
Каждый поток со своими докладами, каждый доклад со спикерами — три уровня, три запроса, сколько бы ни было строк.
tracks, err := db.Tracks.Query().
Where(Tracks.ConferenceID.Eq(confID)).
With(Tracks.Talks.
OrderBy(Talks.StartsAt.Asc()).
With(Talks.Speakers)).
OrderBy(Tracks.Name.Asc()).
All(ctx)Сортировка внутри With — это сортировка самих докладов. Отсортировать их потом
в Go тоже можно, но это значит забрать их в том порядке, в каком их нашёл сервер.
Инвентаризация склада#
Товары, которые ни разу не пересчитывали, — фильтр по отсутствию связи, без загрузки:
uncounted, err := db.Products.Query().
Where(Products.Counts.None()).
OrderBy(Products.SKU.Asc()).
All(ctx)И наоборот, с условием на потомке:
disputed, err := db.Products.Query().
Where(Products.Counts.Any(Counts.Variance.Gt(0))).
All(ctx)Оба компилируются в полусоединения. Ни один не привезёт ни одной строки пересчёта, потому что вы её не просили.
Входящие поддержки#
Открытые обращения только с последним сообщением — работу делает лимит на родителя:
tickets, err := db.Tickets.Query().
Where(Tickets.Status.Eq("open")).
With(Tickets.Messages.
OrderBy(Messages.SentAt.Desc()).
Limit(1)).
OrderBy(Tickets.OpenedAt.Asc()).
All(ctx)Limit(1) относится к обращению, а не к результату. Один запрос возвращает
свежайшее сообщение для каждого из них.