Версионирование, пагинация и ответы N+1
Что это такое
Проблемы приходят вместе с пользователями и данными: опубликованное поле нельзя убрать, не сломав кого-нибудь, поэтому интерфейс должен уметь меняться, а коллекция вырастает за пределы одного ответа, поэтому её нужно разбивать на страницы. Отдельная беда — ответ, который вкладывает связанные ресурсы: сервер соблазняется выдавать по одному запросу на родителя, и стоимость растёт вместе с результатом. В первый день жизни API ничего из этого не видно, а заложить всё заранее гораздо дешевле.
Почему это важно
Эти три вещи дают большую долю жалоб на API, который был корректен на момент выпуска, и каждая из них — решение проектирования с окном, в котором оно дёшево. Ломающее изменение заставляет всех клиентов обновиться разом, что для публичного API практически невозможно; коллекция без пагинации в конце концов упирается в тайм-аут или исчерпывает память; а эндпоинт, число запросов у которого растёт с числом строк, работает на тестах и падает в продакшене на первом же крупном аккаунте.
Ключевые идеи
Добавление безопасно, вычитание — нет
Необязательное поле или новый эндпоинт не ломают существующих клиентов, а вот удаление или переименование поля, смена типа и изменение смысла значения ломают. Большая часть того, что выглядит давлением версий, на самом деле — неспособность делать изменения аддитивными. Если изменение всё же ломает совместимость, версия в адресе или в заголовке оставляет старым клиентам неизменённый путь, пока новые переходят дальше.
добавить поле -> безопасно
сделать поле обязательным -> ломает
расширить тип -> безопасно, если старые значения означают то же
Устаревание — это график, а не объявление
Поле, которое предстоит удалить, сначала помечают, документируют и отслеживают по фактическому использованию, и только потом удаляют. Отслеживание обычно пропускают, а ведь именно оно говорит, сломается ли кто-нибудь. Удаление становится безопасным, когда заголовки об устаревании отправлены, а трафик понаблюдали в течение полного цикла самого медленного клиента.
Пагинация должна быть и в ответе, и в запросе
Коллекция со страницами должна сообщать, как получить следующую, и в идеале — насколько она велика, а сам способ пагинации выбирают по данным. Пагинация со смещением проста, но становится неточной по мере изменения данных; пагинация по курсору остаётся стабильной и лучше подходит для больших или быстро меняющихся коллекций. Особенно важен выбор там, где дубликаты и пропуски недопустимы, например в ленте.
Страница всегда должна быть ограничена
API, который принимает неограниченный размер страницы, находится в одном запросе от исчерпания памяти сервера. Однако максимум предела обязан обеспечивать сам сервер, а не обещание клиента его соблюдать, — и это решение в одну строку предотвращает целый класс инцидентов.
Запросы N+1 рождаются из формы ответа
Сериализация списка элементов и получение для каждого его связанных записей выдаёт по одному запросу на элемент. На десяти строках это незаметно, на десяти тысячах — смертельно, и причина кроется в том, где загружаются данные, а не в базе. Лечится это загрузкой связанных записей для всей страницы одним запросом или возвратом идентификаторов, чтобы клиент сам запросил связанную коллекцию.
Форма ответа — это контракт о стоимости
Включение каждого связанного объекта в каждый ответ делает один вызов удобным. Плата за это — эндпоинт, дорогой для клиентов, которым нужны только идентификаторы. Предсказуемое правило — коллекция содержит идентификаторы, детальный ответ содержит объект, связанные коллекции — отдельные адреса — держит и полезную нагрузку, и число запросов под контролем.
Практика
Спроектируйте контракт страниц
Возьмите эндпоинт коллекции и дайте ему ограниченный размер страницы, ссылку на следующую и явное упорядочение.
Готово, когда: запрос страницы больше максимума обрезается до максимума, а переход по следующей ссылке дважды возвращает каждый элемент ровно один раз, даже когда добавляются новые.
Найдите и исправьте запрос N+1
Возьмите эндпоинт, который возвращает список со связанными данными, посчитайте, сколько запросов он выдаёт, и измените его так, чтобы связанные записи для всей страницы загружались одним запросом.
Готово, когда: число запросов больше не растёт с числом элементов на странице.
Частые ошибки
- Удаление или переименование поля на месте — все клиенты, которые уже есть, ломаются разом; исправить: добавьте замену, объявите старое устаревшим, отследите его использование, затем удалите.
- Приём неограниченного размера страницы — один запрос может исчерпать сервер; исправить: обеспечьте максимум на стороне сервера.
- Пагинация со смещением по быстро меняющимся данным — элементы пропускаются или дублируются между страницами; исправить: используйте курсор для нестабильных коллекций.
- Сериализация вложенных данных по элементу — число запросов растёт со страницей; исправить: загружайте связанные данные пакетом.
- Возврат всего в каждом ответе — полезная нагрузка и стоимость запросов растут вместе; исправить: определите, что включает каждый эндпоинт.
Что читать дальше
- Ресурсы, методы и коды состояния (05-backend-data/06-restful-api/01-resources-verbs-and-status-codes) — правила проектирования, которые проверяют эти режимы отказа.
- RESTful API (05-backend-data/06-restful-api) — стиль, операционной половиной которого является этот дочерний урок.
- SQL и реляционные базы данных (05-backend-data/07-sql-relational-databases) — модель запросов, объясняющая, почему ответы N+1 дороги.