Объектно-реляционное отображение
ORM
Что это такое
Объектно-реляционное отображение — это перевод между объектами, с которыми работает приложение, и строками, которые хранит реляционная база. Отображатель генерирует запросы на загрузку и сохранение, следит за тем, какие объекты изменились, и нередко даёт собственный интерфейс запросов на языке приложения. Абстракция действительно полезна. Однако действительно и протекает: две модели не совпадают, а ошибки селятся именно там, где совпадения нет.
Почему это важно
ORM сегодня стоит в большинстве приложений, поэтому и проблемы производительности, и ошибки корректности в доступе к данным чаще всего обнаруживаются через неё. Повторяющийся код отображатель экономит щедро, зато прячет три вещи, которые как раз и важны: в какой момент уходят запросы, во что обходится обход связи и сколько данных загружается. Если разработчик знает, где абстракция протекает, он умеет пользоваться отображателем там, где тот хорош, и спускаться к явным запросам там, где нет, — именно это отличает быстрое приложение от того, что выдаёт запрос на каждую строку.
Ключевые идеи
Несовпадение моделей реально и структурно
У объектов есть идентичность, ссылки и наследование, а у строк — ключи, внешние ключи и никакого наследования. Ссылка из одного объекта в другой превращается в соединение или во второй запрос, иерархия наследования — в столбец-дискриминатор либо в несколько таблиц. За перевод отвечает выбор отображателя, и этот выбор сказывается на числе и форме запросов так, что по коду приложения его не видно.
order.customer.name -> может быть одно соединение или один лишний запрос на заказ
Ленивая загрузка — самое болезненное протекание
Связь обычно подгружается только в момент обращения: так удобно, но каждый обход в цикле оборачивается запросом на итерацию. Строка кода читается как обращение к свойству, а ведёт себя как сетевой вызов. Отсюда и шаблон N+1 — самый частый дефект производительности в приложениях на ORM; лечится он загрузкой связи вместе с исходным запросом или пакетно.
Единица работы решает, когда выполняются инструкции
Отображатель, как правило, копит изменения и сбрасывает их на границе. Значит, запись уйдёт позже строки, которая её вызвала, и уже вне контроля приложения, а потому явная транзакция или требуемый порядок вокруг неё не гарантированы. Чтобы осознанно поместить работу внутрь транзакции, надо знать, где происходит сброс и что его запускает.
Внутри одной сессии отображатель возвращает для одной и той же строки один и тот же объект и помнит его состояние. Эта идентичность полезна, но из неё следует, что сессию нужно завершать: иначе кэшированные объекты растут без предела, а устаревшие данные переиспользуются. Как очертить область сессии — один запрос или одна операция, — решение проекта, и у него есть последствия для корректности.
Интерфейс запросов — отдельный язык со своими ловушками
Многие отображатели позволяют строить запросы прямо на языке-хосте. Это удобно: результат может оказаться ровно тем SQL, который задуман, — а может и по запросу на каждый элемент, смотря как потреблять результаты.
Аварийные выходы — часть замысла
Явную инструкцию или написанный вручную запрос умеет выполнить любой отображатель. Прибегнуть к этому на тех немногих операциях, где сгенерированный запрос неверен, — не поражение, а верное обращение с протекающей абстракцией. Если держать такие выходы в одном слое, остальное приложение о них не знает.
Схема и код меняются вместе, и миграция обязательна
Модель отображателя — второе описание схемы, и разойтись они могут. Поэтому миграцию, меняющую схему, сопровождают изменением модели и развёртыванием, которое терпит обе версии во время перехода.
Практика
Посчитайте запросы, которые выдаёт обход
Возьмите список, где для каждого элемента показывается связанное значение, и посчитайте, сколько инструкций выдаёт отображатель.
Готово, когда: вы можете сказать, растёт ли число с количеством элементов, и указать сгенерированную инструкцию, которая повторилась.
Перенесите сброс внутрь его транзакции
Найдите место, где изменение сбрасывается на границе, а не на строке, которая его вызвала, и добейтесь, чтобы запись происходила внутри транзакции, которая обязана её содержать.
Готово, когда: фиксация транзакции сохраняет ровно задуманные изменения, а сбой до фиксации не сохраняет ничего.
Частые ошибки
- Обход связи внутри цикла — код читается как доступ к свойству и выдаёт запрос на итерацию; исправить: загружайте связь вместе с исходным запросом.
- Предположение, что запись происходит на вызвавшей её строке — сброс случается на границе, возможно вне транзакции; исправить: знайте точку сброса и размещайте её осознанно.
- Отсутствие взгляда на сгенерированную инструкцию — абстракции доверяют вслепую; исправить: включайте журналирование инструкций при разработке и просматривайте его.
- Слишком долго живущая сессия — карта идентичности растёт, устаревшие объекты переиспользуются; исправить: ограничьте сессию одним запросом или операцией.
- Отношение к модели отображателя как к схеме — нужна написанная вручную миграция, а модель устарела; исправить: держите миграции явными, а модель — в согласии с ними.
Что читать дальше
- SQL и реляционные базы данных (05-backend-data/07-sql-relational-databases) — модель, которую скрывает эта абстракция, поэтому читайте её первой.
- Prisma ORM (05-backend-data/19-prisma-orm) — конкретный отображатель для Node и TypeScript и его собственная версия этих протечек.
- Индексы и планы запросов (05-backend-data/07-sql-relational-databases/03-indexes-and-query-plans) — как проверить, что сгенерированные инструкции действительно делают.