Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 12 из 25 ~18 мин +50 XP

Пагинация и сортировка

Десятый урок научил отбирать строки предикатами, одиннадцатый — привозить соседей With-вызовами, но оба возвращали выборку целиком: страницей она ещё не стала. Сегодня — ent пагинация и сортировка: пара Limit и Offset, которая живёт в билдере с самого начала, честная математика страниц через Count из девятого урока, цена глубоких offset-ов и их дрейфа, а затем курсорная пагинация — keyset-подход, где страница задаётся не номером, а границей. Отдельный сюжет — order ent во всех подробностях: несколько ent.Asc и ent.Desc в одном запросе, проблема равных значений без тай-брейкера и обещание девятого урока про упорядочивание по числу соседей-рёбер. Если вы гуглили ent limit offset и находили только фрагменты — здесь механика собрана целиком на каноне курса: анна, борис, вера и их посты.

Содержание урока · 13 разделов

Маршрут урока — от наивной страницы к промышленной. Первый сюжет — ent пагинация в механике Limit и Offset: что именно они пишут в билдер и в SQL, и почему страница — это всегда тройка «сортировка, окно, исполнение». Второй — order ent в развёрнутом виде: несколько полей за один вызов, стабильность порядка и тай-брейкер по id, без которого страницы начинают дублировать и терять строки. Третий — подсчёт страниц через Count и форма пагинационного ответа, которую ждёт любой фронтенд. Четвёртый — цена глубоких offset-ов: сколько строк база читает и выбрасывает, и почему данные, изменившиеся между запросами, сдвигают окна. Пятый — курсорная пагинация: страница как граница, курсор по первичному ключу и составной курсор по паре «время, идентификатор». Шестой — обещание девятого урока: упорядочивание по числу соседей-рёбер через Modify. Финиш — таблица выбора между offset и курсором. Данные прежние: анна (26 лет, два поста — «про рёбра» и «черновик»), борис (19), вера (17), группа go-ninja.

ent limit offset: что именно делает пара Limit и Offset#

С девятого урока вы знаете, что Limit(n) и Offset(k) копятся в билдере так же, как предикаты Where, — и исполняются только вместе с All(ctx). Теперь достроим механику страницы целиком. В SQL запрос с пагинацией состоит из трёх обязательных частей: ORDER BY задаёт порядок строк, LIMIT отрезает окно, OFFSET пропускает начало. Уберёте сортировку — получите окно поверх недетерминированного порядка, и на каждой перезагрузке страницы состав может гулять; забудете OFFSET — каждый раз вернётся первая порция. ent тут без волшебства: Limit и Offset — прозрачные методы, которые рендерятся в одноимённые клаузы SQL, и за содержимое окна отвечает ваша связка с Order. Нюанс из девятого урока вспоминаем одной строкой: OFFSET без LIMIT в SQL не живёт, и ent при заданном offset сам подставляет лимит-максимум — это было в генераторе, страница вам его повторять не будет. Отсюда рабочее правило ent limit offset: любую страницу собирайте полной тройкой — Order, Limit, Offset — и никогда не полагайтесь на «естественный» порядок таблицы: его не существует ни в одной СУБД.

Первая страница пользователей

GO
// страница 0: первые десять пользователей по убыванию возраста;
// при равном возрасте — по возрастанию id, чтобы порядок был строгим:
users, err := client.User.Query().
        Order(ent.Desc(user.FieldAge), ent.Asc(user.FieldID)).
        Limit(10).
        Offset(0).
        All(ctx)
if err != nil {
        return err
}

// страница 1 — тот же порядок, окно сдвинуто:
page2, err := client.User.Query().
        Order(ent.Desc(user.FieldAge), ent.Asc(user.FieldID)).
        Limit(10).
        Offset(10).
        All(ctx)

Оба вызова — один и тот же билдер с разными Offsets: предикатов нет, значит выборка — вся таблица users. Связка с одиннадцатым уроком прямая: With-вызовы и пагинация свободно смешиваются в одной цепочке — загрузить соседей можно и для окна из десяти строк, тариф жадной загрузки при этом не меняется, один запрос на ребро. Notice-момент: Offset(0) можно и не писать — дефолт нулевой, но явный ноль в коде страницы читается лучше.

Order: несколько полей и тай-брейкер#

Сортировка в ent — метод Order на query-билдере, и его аргументы — не строки, а хелперы корневого пакета: ent.Asc и ent.Desc. Каждый принимает вариативный список констант-колонок, поэтому два вида записи эквивалентны: Order(ent.Desc(user.FieldAge), ent.Asc(user.FieldID)) и Order(ent.Desc(user.FieldAge, user.FieldID)) — разве что направление во втором случае общее, а в первом своё на поле. Несколько вызовов Order на одном билдере складываются: последний добавляет поля в конец ORDER BY, а не затирает прежние — в этом отличие от перезаписи With-полей из одиннадцатого урока. Валидация та же, что вы видели у ent.Desc в девятом: выдуманное имя колонки отвергается до базы, потому что константы типа user.FieldName — это ключи ValidColumn, а не свободный текст. Теперь главное — тай-брейкер. ORDER BY age DESC для трёх пользователей вернёт однозначный порядок, а для тысячи — уже нет: при равных возрастах база вольна вернуть строки в любой последовательности, и она действительно меняет её от запроса к запросу. На одной странице это незаметно, на пагинации — беда: строка с возрастом 26 может попасть и в страницу 0, и в страницу 2, а какая-то — не попасть никуда. Лечение простое и обязательное: последним полем сортировки ставьте уникальную колонку — на каноне курса id: Order(ent.Desc(user.FieldAge), ent.Asc(user.FieldID)). Теперь каждая строка занимает строгое место, и окно OFFSET не может вернуть её дважды.

Проверка: как ляжет ORDER BY

Запрос: Order(ent.Desc(user.FieldAge), ent.Asc(user.FieldID)).Limit(10). Что именно гарантирует такая сортировка?

Математика страниц: Count#

Пользовательский интерфейс хочет больше, чем просто окно: «страница 2 из 7», «всего 63». Число страниц добывается тем же запросом, что и данные, минус окно: Count(ctx) из девятого урока — он переиспользует весь WHERE вашего билдера и вычищает ORDER BY, потому что сортировка внутри счётного запроса некоторым базам запрещена. Дальше арифметика: всего страниц — ceil(total / limit), последняя страница — та, где offset + limit >= total. Два практических замечания. Первое: Count — это отдельный поход в базу, и на больших таблицах он не бесплатен; для «показать ещё» с курсором он вообще не нужен, и ниже мы этим воспользуемся. Второе: между запросом Count и запросом страницы данные могли измениться — total честный только на момент своего запроса, и это норма всей клиент-серверной жизни, а не особенность ent. Собственно подсчёт не требует от билдера ничего нового: тот же Query, те же предикаты из десятого урока, другой терминальный метод. Комбинируя Count, Limit и Offset, вы получаете классический «нумерованный» пагинатор, которого достаточно для большинства админок и каталогов.

Нумерованный пагинатор на Count

GO
// параметры страницы приходят снаружи:
const (
        perPage = 10
        page    = 2 // третья страница, считая с нуля
)

// 1. сколько всего строк под фильтр (WHERE переиспользуется):
total, err := client.User.Query().
        Where(user.AgeGTE(18)).
        Count(ctx)
if err != nil {
        return err
}

// 2. само окно — те же предикаты, плюс сортировка:
items, err := client.User.Query().
        Where(user.AgeGTE(18)).
        Order(ent.Desc(user.FieldAge), ent.Asc(user.FieldID)).
        Limit(perPage).
        Offset(page * perPage).
        All(ctx)

pages := (total + perPage - 1) / perPage // ceil без математики

Оба запроса несут один и тот же Where — фильтр согласован между счётом и содержимым. Целочисленный ceil (total + perPage - 1) / perPage — идиома без вызовов math. На каноне курса: взрослых двое — анна и борис, вере 17; при perPage = 1 total равен двум и pages тоже двум. Для пустого фильтра Count вернёт 0 — страниц нет, но и ошибки нет: «пусто» — данные, а не сбой.

Что кладут в пагинационный ответ#

  • items — срез сущностей страницы; сюда же после запроса «пристёгиваются» рёбра With-вызовами из одиннадцатого урока.
  • total — общее число строк под фильтр; добывается Count-ом с тем же WHERE; для курсорной пагинации часто опускается.
  • page и perPage — эхо входных параметров; фронтенд сверяет их со своими стейтами, не доверяя молчанию.
  • pages — ceil(total / perPage); для курсоров вместо него отдают hasNext.
  • next и prev — ссылки или курсоры следующей и предыдущей страницы; курсор обычно кодирует сортировочный ключ последней строки.

Глубокие страницы: чем платит OFFSET#

У нумерованной пагинации есть цена, и растёт она линейно с номером страницы. OFFSET k в SQL означает: прочитать первые k + limit строк по порядку сортировки и выбросить первые k. Не «перейти» — прочитать и выбросить: база обязана пройти начало выборки каждый раз, потому что окно определяется позиционно. OFFSET 1990 при LIMIT 10 — две тысячи прочитанных строк, десять отданных; OFFSET 999900 — почти миллион. Индекс по сортировочному полю превращает это из «прочитать таблицу» в «пройти по индексу», но константа всё равно остаётся: глубина страницы — это время. Вторая беда тоньше — дрейф. Между запросом страницы 0 и страницы 1 кто-то вставил нового пользователя — все окна сместились на одну строку: анна, стоявшая последней на странице 0, появится снова первой на странице 1. Удалили строку — часть строк страницы 1 не попадёт ни в одну страницу. Тай-брейкер из прошлого блока делает порядок строгим, но не отменяет позиционности: OFFSET считает строки, а не значения. Поэтому в нагруженных списках — ленты, фиды, бесконечные прокрутки — нумерацию страниц заменяют курсорами, и это следующий сюжет. Если вы уже читали курс GORM, его семнадцатый урок показывал ту же развилку на другой ORM: синтаксис меняется, дилемма — нет.

Сколько строк прочитает база на глубокой странице

Прочитайте код и выберите, что он выведет:

// страница 199 при perPage = 10 (offset = 1990):
users, err := client.User.Query().
        Order(ent.Asc(user.FieldID)).
        Limit(10).
        Offset(1990).
        All(ctx)

// в таблице 300 000 пользователей.
// сколько строк база прочитает по порядку, чтобы отдать 10?

Курсорная пагинация: страница как граница#

Курсорная (она же keyset) пагинация меняет саму постановку вопроса. Вместо «дай строки 20–29» клиент просит «дай следующие 10 строк после вот этой» — и «вот эта» кодируется значением сортировочного ключа последней полученной строки. Для сортировки по возрастанию id страница — это WHERE id > lastID в паре с ORDER BY id ASC и LIMIT: предикат IDGT из каталога десятого урока. Вся конструкция опирается на индекс по первичному ключу, поэтому каждый переход между страницами стоит одинаково — глубина страницы перестала существовать. Дрейф тоже исчезает: строки вставляются с растущими id, и новая запись не сдвигает чужие окна — она либо попадёт в конец ленты, либо нет, но уже выданные страницы не перетасуются. Плата за это — два отказа. Первый: нельзя прыгнуть на произвольную страницу, потому что «страница 7» не определена без позиции; курсорные интерфейсы — это «показать ещё» и стрелки, а не нумерованные списки. Второй: без Count вы не знаете общего числа строк — либо считайте отдельным запросом, либо честно скажите дизайнеру, что «всего 63» в бесконечной ленте никому не нужно. Курсор — это просто значение, которое клиент вернёт вам в следующем запросе; сериализуете вы его в base64 или отдаёте голым числом — ваше API-решение, ent ограничивается предикатом.

Курсор по первичному ключу

GO
// первая страница: обычное окно без предиката:
page1, err := client.User.Query().
        Order(ent.Asc(user.FieldID)).
        Limit(10).
        All(ctx)

// курсор — id последней строки страницы:
cursor := page1[len(page1)-1].ID

// следующая страница: всё после границы, окно того же размера:
page2, err := client.User.Query().
        Where(user.IDGT(cursor)).
        Order(ent.Asc(user.FieldID)).
        Limit(10).
        All(ctx)

// трюк «limit+1»: запросить 11, если вернулось 11 —
// hasNext = true, а одиннадцатую строку не отдаём

IDGT — обычный сгенерированный предикат where.go, тот же класс, что AgeGTE из десятого урока: просто здесь он сравнивает первичный ключ. Запрос «следующей страницы» настолько же дёшев, насколько дёшев lookup по индексу, — и одинаков на любой глубине. Трюк limit+1 решает вопрос hasNext без Count: запросите на одну строку больше, увидели переполнение — есть продолжение. Канон: анна id 1, борис id 2, вера id 3 — курсор 2 вернёт страницу с одной верой при LIMIT 10.

Курсор по не-id колонке: составной ключ#

Сортировать только по id — редкая роскошь: ленты обычно упорядочены по времени. И здесь подстерегает ловушка, которая ломает наивные курсоры: created_at не уникален. Два поста, написанные в одну секунду, — норма; курсор вида «после 12:00:00» с WHERE created_at > t молча выбросит второй пост с тем же временем. Лечение — составной курсор: сортируем по паре (created_at, id) и границей считаем тоже пару. Условие «строка строго после границы» читается как: время меньше по значению ИЛИ время то же, но id больше — в предикатах ent это user.Or(user.CreatedAtLT(cursor.Time), user.And(user.CreatedAtEqual(cursor.Time), user.IDGT(cursor.ID))). Запись многословная, но логика — ровно сравнение кортежей, как в математике: (t1, id1) > (t2, id2) означает t1 > t2 ИЛИ (t1 = t2 И id1 > id2). База такой WHERE исполняет по индексу (created_at, id) — составной индекс из двадцатого урока для этого и рождается. Ошибка «взять только время» проявляет себя не сразу: пока у вас одна строка в секунду, всё работает; под нагрузкой посты начинают теряться. Запомните правило: курсор уникален тогда и только тогда, когда сортировочный ключ уникален; не уникален — достраивайте состав.

Проверка: зачем в курсоре второе поле

Лента отсортирована по created_at DESC. Почему одного предиката CreatedAtLT(t) для следующей страницы мало?

Составной курсор на (created_at, id)

GO
// лента постов анны по убыванию времени:
page1, err := client.Post.Query().
        Order(ent.Desc(post.FieldCreatedAt), ent.Desc(post.FieldID)).
        Limit(5).
        All(ctx)

// курсор — кортеж последней строки:
last := page1[len(page1)-1]

// следующая страница: строго «после» кортежа:
page2, err := client.Post.Query().
        Where(post.Or(
                post.CreatedAtLT(last.CreatedAt),
                post.And(
                        post.CreatedAtEqual(last.CreatedAt),
                        post.IDGT(last.ID),
                ),
        )).
        Order(ent.Desc(post.FieldCreatedAt), ent.Desc(post.FieldID)).
        Limit(5).
        All(ctx)

Сортировка и курсор зеркалят друг друга: DESC по обоим полям — и предикат «строго после» по тем же полям. Направления сортировки и знаки сравнения обязаны совпадать: поменяете порядок на ASC, а курсор оставите — страницы поедут в другую сторону, и границы перестанут значить то, что вы думали. Число запросов на страницу не изменилось: один SELECT на данные, плюс отдельный COUNT, если UI настаивает на «всего».

Сортировка по числу соседей: обещание девятого урока#

Девятый урок оставил вам обещание: покажем, как упорядочить пользователей по числу их постов. Готового API у ent для этого нет — Order принимает только колонки, а «число соседей» колонкой не является. Два рабочих пути. Первый — денормализация: поле posts_count в схеме, которое поддерживают хуки или групповые обновления; сортировка превращается в обычный Order(ent.Desc(user.FieldPostsCount)) и достаётся бесплатно при любом объёме, но требует дисциплины — счётчик надо не забывать двигать при создании и удалении постов, за этим к семнадцатому уроку. Второй путь — Modify из feature-флага sql/modifier (флаги включались в седьмом уроке): он даёт доступ к финальному SELECT-у через sql.Selector, где можно докрутить JOIN, GROUP BY и выражение сортировки. Это уже почти ручной SQL — с типобезопасностью билдера на корневом запросе, но с полной свободой внутри модификатора. Такой запрос стоит держать под присмотром: план с GROUP BY по большой таблице дороже любого курсора, и для «топа авторов» честнее посчитать денормализованно. Показываю механику — решение о применении принимайте по своему объёму данных.

Modify: топ авторов по числу постов

GO
// требует флага sql/modifier (седьмой урок):
users, err := client.User.Query().
        Modify(func(s *sql.Selector) {
                s.LeftJoin(sql.Table("posts")).
                        On(s.C(user.FieldID), post.FieldAuthorID).
                        GroupBy(s.C(user.FieldID)).
                        OrderBy(sql.Desc(sql.Count(post.FieldID)))
        }).
        All(ctx)

// SQL: SELECT users.* FROM users
//      LEFT JOIN posts ON users.id = posts.author_id
//      GROUP BY users.id
//      ORDER BY COUNT(posts.id) DESC

Modify получает финальный селектор — тот самый, что обычно рендерится из билдера. LeftJoin с On по колонке ребра собирает пары «пользователь — его посты», GroupBy сворачивает их в пользователя, OrderBy по COUNT расставляет по активности. У веры ноль постов — LEFT JOIN гарантирует, что она останется в списке с нулём, в отличие от INNER JOIN. GROUP BY по первичному ключу легален в PostgreSQL (функциональная зависимость) и MySQL; всё же лишнее в SELECT ent сам вычищать не станет — проверяйте план запроса на реальном объёме.

Offset против курсора: сводка выбора#

КритерийLimit + OffsetКурсор (keyset)
Произвольный переходда: страница N вычисляется offset = N * perPageнет: только «следующая» и «предыдущая»
Стоимость глубокой страницырастёт с offset: база читает и выбрасывает offset + limit строкконстантная: предикат по индексу, глубина не важна
Дрейф при вставках и удаленияхокна сдвигаются: строки дублируются и теряются между страницаминовые строки не сдвигают уже выданные страницы
Общее число строкиз коробки через Count с тем же WHEREтолько отдельным Count или никак
Требования к порядкуобязателен тай-брейкер по уникальной колонкекурсор уникален ровно настолько, насколько уникален ключ
Типовые примененияадминки, каталоги, отчёты с номерами страницленты, фиды, бесконечные прокрутки, API для мобильных клиентов

Проверка: выбрать стратегию

Мобильный клиент бесконечно скроллит ленту постов, вставки идут каждую секунду, номера страниц в дизайне нет. Что выбрать?

Соберите курсорную страницу

Впишите сортировку по убыванию id, предикат курсора, размер окна и подсчёт страниц.

// страница после курсора lastID: page, err := client.User.Query(). Where(user.(lastID)). // строго после границы Order(ent.(user.FieldID)). // по возрастанию id (10). // окно страницы All(ctx) // а общее число строк под фильтр даёт: total, err := client.User.Query().(ctx)

Путь одной курсорной страницы

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

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Что дальше#

Вы умеете резать выборку на страницы обоими способами: нумерованные окна с честной математикой Count и keyset-курсоры, которые не боятся ни глубины, ни вставок; сортировка теперь в вашем распоряжении целиком — от мультиполей с тай-брейкером до сортировки по числу соседей. Но всё, что вы вернули клиенту, он пока может только читать: строки было нечем менять. Двенадцатый урок закрыл чтение, тринадцатый — 13-update — откроет запись: UpdateOneID с Set-мутаторами, массовое обновление по предикатам и возврат обновлённой сущности, а в придачу и upsert-развилка, чьи азы лежат в восьмом уроке. Домашнее задание этого урока — перевести свой список пользователей с «всё на одной странице» на обе стратегии и почувствовать разницу в логе драйвера. Если понадобится вспомнить каталог предикатов для курсоров — он в десятом уроке, а механика жадной загрузки соседей на странице — в одиннадцатом. Полный план из 25 уроков — на странице курса.

Карточки: пагинация и сортировка

Термин

Минимальная честная страница в ent

нажмите, чтобы перевернуть
Определение

Тройка Order + Limit + Offset: сортировка задаёт строгий порядок (последним полем — уникальный id), LIMIT отрезает окно, OFFSET пропускает начало. OFFSET без LIMIT в SQL не живёт — ent сам подставит лимит-максимум. Без Order состав страницы недетерминирован.

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Пагинация в ent — это тройка Order, Limit, Offset поверх любого билдера: предикаты десятого урока фильтруют, окно отрезает LIMIT, порядок задаёт ORDER BY. Ent limit offset рендерится в одноимённые SQL-клаузы без магии; OFFSET без LIMIT ent достраивает лимитом-максимумом.
  • Сортировка order ent: ent.Asc и ent.Desc принимают вариативные списки колонок, несколько вызовов Order складываются в конец ORDER BY. Валидация колонок происходит до базы. Тай-брейкер обязателен: без уникального поля в конце строки дублируются и теряются между страницами.
  • Подсчёт страниц — Count(ctx) с тем же WHERE: предикаты переиспользуются, ORDER BY вычищается. pages = ceil(total / perPage). Count — отдельный запрос, честный только на свой момент времени.
  • Глубокие offset-ы читают и выбрасывают offset + limit строк — стоимость растёт линейно с номером страницы; вставки и удаления сдвигают позиционные окна (дрейф). Курсорная пагинация убирает обе беды ценой отказа от произвольного перехода и total.
  • Курсор — значение сортировочного ключа последней строки: Where(user.IDGT(cursor)) + Order + Limit. hasNext добывается трюком limit+1. Курсор уникален ровно настолько, насколько уникален ключ сортировки; для created_at нужен составной курсор Or(CreatedAtLT, And(CreatedAtEqual, IDGT)).
  • Сортировка по числу соседей-рёбер готового API не имеет: либо денормализованный счётчик, поддерживаемый хуками, либо Modify из флага sql/modifier с LeftJoin, GroupBy и OrderBy(sql.Desc(sql.Count(...))).
  • Выбор стратегии: нумерованные страницы — для админок и каталогов, курсоры — для лент и бесконечных прокруток. В GORM та же дилемма разобрана в его семнадцатом уроке: синтаксис меняется, механика — нет.
  • Пагинация свободно комбинируется с жадной загрузкой из одиннадцатого урока: With-вызовы на странице из десяти строк стоят столько же запросов, сколько на всей таблице, — один на ребро.

Домашнее задание

Работайте на базе из восьмого урока — анна, борис, вера, их посты и группа go-ninja — и включите лог SQL драйвера. Первое: соберите нумерованный пагинатор по возрасту с тай-брейкером по id, perPage = 1, и проверьте математику страниц: adults — двое, значит pages = 2; пройдите обе страницы и убедитесь, что ни одна строка не повторилась. Второе: умышленно уберите тай-брейкер и поищите недетерминизм — на трёх пользователях его может не быть, тогда отсортируйте по role (у анны админ, у бориса и веры юзер) и посмотрите в лог, что именно база вернула при равных значениях. Третье: смоделируйте дрейф — прочитайте страницу 0 нумерованной пагинации, вставьте пользователя с возрастом больше всех, прочитайте страницу 1 и найдите дубль; затем повторите то же на курсоре по id и убедитесь, что уже выданные страницы не изменились. Четвёртое: соберите составной курсор по постам (created_at, id) — создайте два поста с одним временем и проверьте, что оба доезжают по разные стороны границы; сверьте с логом, что сортировка и предикат зеркальны. Пятое: посчитайте соотношение прочитанных и отданных строк на OFFSET 2 при трёх пользователях, затем доведите таблицу до хотя бы сотни строк CreateBulk-ом из восьмого урока и сравните план запроса страницы 0 и страницы 9. Вопрос на подумать: почему курсор по неуникальному полю «молча» теряет строки, и какие ещё пары полей на нашей схеме годятся в составной курсор?

Личные заметкиПусто
Хранятся только в вашем браузере (ent · Пагинация и сортировка)

Вопросы и ответы по уроку

Можно ли в ent сделать пагинацию без сортировки?

Технически — да: Limit и Offset работают и без Order. Фактически — нет: без ORDER BY порядок строк не гарантирован ничем, и страницы будут дублировать и терять строки. Единственная безопасная сортировка без явного поля — по первичному ключу, потому что он уникален и индексирован; всё остальное требует тай-брейкера.

Как совместить курсорную пагинацию с фильтрами и жадной загрузкой?

Свободно: предикат курсора — обычный предикат, он складывается с Where из десятого урока через And, а With-вызовы одиннадцатого урока работают на любой выборке, включая окно страницы. Тариф не меняется: один запрос на данные плюс один на каждое запрошенное ребро. Следите только за тем, чтобы фильтры не менялись между страницами — иначе курсор отсчитывает строки в другой выборке.

Нужно ли всегда отдавать total и pages?

Нет, и часто это правильно. Count на больших таблицах — отдельный полноценный запрос, а в лентах «всего 63 214 постов» не имеет операционной ценности: данные изменились раньше, чем вы дочитали число. Нумерованные интерфейсы (админка, отчёт) без total не живут; курсорные интерфейсы обходятся hasNext через трюк limit+1. Решение принимает UX, а не ORM.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Сравните с альтернативами

Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.

Следующая ступень учебного пути

go-kit25 уроков~14 чпродвинутый25 уроков уже доступны

go-kit: микросервисный тулкит

Курс go-kit: endpoint, транспорт, логирование, метрики, трейсинг, circuit breaker, service discovery. 25 уроков с итоговым микросервисом.

  • Понимать архитектуру endpoint → transport → service
  • Оборачивать сервисы middleware: логи, метрики, трейсинг
  • Подключать HTTP и gRPC транспорты
  • Ставить rate limiting и circuit breaker