awesome-everything EN
↑ Обратно к восхождению

API

Контракты batch-функции: порядок, формы, ошибки

Суть batchLoadFn должна возвращать значения в том же порядке, что входные ключи, явно обрабатывать отсутствующие строки и per-key ошибки, и поддерживать формы one-to-one, one-to-many и count.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 12 min

Sven подключает DataLoader и страница быстрая — для большинства запросов. Иногда посты показывают не того автора. SQL-запрос выполнился правильно и вернул верные строки. Баг в batch-функции: она вернула строки в том порядке, что выбрал Postgres, а не в том, что ожидал DataLoader.

Контракт порядка

batchLoadFn(keys: Array<K>): Promise<Array<V | Error>> должна вернуть массив той же длины, что keys, с значениями на соответствующих позициях. Если keys = [7, 9, 3], возврат должен быть [row7, row9, row3] — в этом порядке.

Postgres (и большинство баз) не гарантируют порядок строк из WHERE id IN (...). Если запрос вернул [row9, row7, row3] и ты возвращаешь этот массив напрямую, DataLoader разрешает:

  • ключ 7 → row9 (неверно)
  • ключ 9 → row7 (неверно)
  • ключ 3 → row3 (верно случайно)

Это тихий баг: никакой ошибки, тесты могут пройти, в проде — неверные авторы на случайных запросах.

Решение: строй Map из результатов, потом проходи входные ключи.

async function batchAuthors(ids) {
  const rows = await db.query(
    'SELECT id, name FROM users WHERE id = ANY($1)', [ids]
  );
  const map = new Map(rows.map(r => [r.id, r]));
  // Обходи ids в исходном порядке; null для отсутствующих строк
  return ids.map(id => map.get(id) ?? null);
}

Обработка отсутствующих строк

Если у ключа нет строки в базе, map.get(id) возвращает undefined. Возврат undefined в слоте разрешает Promise в undefined, которое попадает в резолвер как значение поля. Для non-nullable GraphQL-полей это runtime-ошибка валидации.

Правильный подход зависит от nullable-настройки поля:

  • Nullable-поле: вернуть null для отсутствующих строк.
  • Non-nullable поле, которое всегда должно существовать: вернуть new Error('User not found: ' + id).

Возврат Error в слоте заставляет DataLoader отклонить только этот конкретный .load()-Promise, а не все ожидающие. Это противоположность throw из batch-функции, который отклоняет все ожидающие Promise.

Форма one-to-many

Для поля Post.tags ключ загрузчика — ID поста, значение — массив тегов. Пустые массивы для постов без тегов обязательны — если вернуть undefined или пропустить слот, теги следующего поста заполнят его место.

async function batchTags(postIds) {
  const rows = await db.query(
    'SELECT post_id, tag FROM tags WHERE post_id = ANY($1)', [postIds]
  );
  // Предзаполни каждый ключ пустым массивом
  const map = new Map(postIds.map(id => [id, []]));
  rows.forEach(r => map.get(r.post_id).push(r.tag));
  return postIds.map(id => map.get(id));
}

Форма count

Для Post.likeCount батч выполняет GROUP BY post_id COUNT(*) и возвращает число (по умолчанию 0 для постов без лайков).

ФормаКлючВозврат на ключПаттерн
One-to-oneID сущностистрока или nullauthorLoader
One-to-manyID родителямассив (возможно пустой)tagsLoader
CountID родителячисло (возможно 0)likeCountLoader

maxBatchSize

Когда один GraphQL-документ ссылается на 5000 уникальных авторов, один WHERE id IN (...) с 5000 ID медленнее, чем пять запросов по 1000 из-за накладных расходов планировщика Postgres. Устанавливай options.maxBatchSize (типично: 500–1000) для автоматического разбиения больших батчей.

Почему это работает

Почему DataLoader позволяет вернуть Error на слот, а не бросить throw? Потому что throw отклоняет все ожидающие .load()-Promise — каждый резолвер в запросе получает rejection, даже те, чьи строки вернулись успешно. Per-slot ошибки дают точечный отказ: одно поле с отсутствующей строкой возвращает ошибку, все остальные разрешаются нормально. Apollo Server переводит ошибку резолвера в partial GraphQL response с одним null-полем и одной error-записью.

Викторина

Batch-функция возвращает строки в том порядке, что вернула база, без Map-построения. Каков симптом?

Викторина

One-to-many batch-функция для Post.tags пропускает пустые массивы для постов без тегов. Что ломается?

Викторина

Batch-функция бросает необработанное исключение. Какие .load()-Promise отклоняются?

Вспомните перед уходом
  1. 01
    Почему batchLoadFn должна возвращать значения в том же порядке, что входные ключи?
  2. 02
    В чём разница между возвратом Error в слоте и throw из batchLoadFn?
  3. 03
    Что нужно вернуть для родительского ключа без детей в one-to-many загрузчике?
Итог

Контракт batch-функции: три правила — та же длина, что входные ключи; значения в том же порядке, что входные ключи; значение (или Error) на каждый ключ. Нарушение порядка тихо возвращает не те строки — DataLoader доверяет позиции. Нарушение длины сдвигает все последующие резолверы. Пропуск пустых массивов в one-to-many батчах передаёт данные одного родителя другому. Возвращай per-slot Error для known-bad строк, чтобы другие резолверы в том же батче прошли успешно; throw — только когда весь батч не может выполниться.

Связанные уроки
встречается в178
Продолжить восхождение ↑Federation и lookahead: батчинг за пределами DataLoader
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.