Объектно-реляционное отображение

ORM

Что это такое

Объектно-реляционное отображение — это перевод между объектами, с которыми работает приложение, и строками, которые хранит реляционная база. Отображатель генерирует запросы на загрузку и сохранение, следит за тем, какие объекты изменились, и нередко даёт собственный интерфейс запросов на языке приложения. Абстракция действительно полезна. Однако действительно и протекает: две модели не совпадают, а ошибки селятся именно там, где совпадения нет.

Почему это важно

ORM сегодня стоит в большинстве приложений, поэтому и проблемы производительности, и ошибки корректности в доступе к данным чаще всего обнаруживаются через неё. Повторяющийся код отображатель экономит щедро, зато прячет три вещи, которые как раз и важны: в какой момент уходят запросы, во что обходится обход связи и сколько данных загружается. Если разработчик знает, где абстракция протекает, он умеет пользоваться отображателем там, где тот хорош, и спускаться к явным запросам там, где нет, — именно это отличает быстрое приложение от того, что выдаёт запрос на каждую строку.

Ключевые идеи

Несовпадение моделей реально и структурно

У объектов есть идентичность, ссылки и наследование, а у строк — ключи, внешние ключи и никакого наследования. Ссылка из одного объекта в другой превращается в соединение или во второй запрос, иерархия наследования — в столбец-дискриминатор либо в несколько таблиц. За перевод отвечает выбор отображателя, и этот выбор сказывается на числе и форме запросов так, что по коду приложения его не видно.

order.customer.name   -> может быть одно соединение или один лишний запрос на заказ

Ленивая загрузка — самое болезненное протекание

Связь обычно подгружается только в момент обращения: так удобно, но каждый обход в цикле оборачивается запросом на итерацию. Строка кода читается как обращение к свойству, а ведёт себя как сетевой вызов. Отсюда и шаблон N+1 — самый частый дефект производительности в приложениях на ORM; лечится он загрузкой связи вместе с исходным запросом или пакетно.

Единица работы решает, когда выполняются инструкции

Отображатель, как правило, копит изменения и сбрасывает их на границе. Значит, запись уйдёт позже строки, которая её вызвала, и уже вне контроля приложения, а потому явная транзакция или требуемый порядок вокруг неё не гарантированы. Чтобы осознанно поместить работу внутрь транзакции, надо знать, где происходит сброс и что его запускает.

Внутри одной сессии отображатель возвращает для одной и той же строки один и тот же объект и помнит его состояние. Эта идентичность полезна, но из неё следует, что сессию нужно завершать: иначе кэшированные объекты растут без предела, а устаревшие данные переиспользуются. Как очертить область сессии — один запрос или одна операция, — решение проекта, и у него есть последствия для корректности.

Интерфейс запросов — отдельный язык со своими ловушками

Многие отображатели позволяют строить запросы прямо на языке-хосте. Это удобно: результат может оказаться ровно тем SQL, который задуман, — а может и по запросу на каждый элемент, смотря как потреблять результаты.

Аварийные выходы — часть замысла

Явную инструкцию или написанный вручную запрос умеет выполнить любой отображатель. Прибегнуть к этому на тех немногих операциях, где сгенерированный запрос неверен, — не поражение, а верное обращение с протекающей абстракцией. Если держать такие выходы в одном слое, остальное приложение о них не знает.

Схема и код меняются вместе, и миграция обязательна

Модель отображателя — второе описание схемы, и разойтись они могут. Поэтому миграцию, меняющую схему, сопровождают изменением модели и развёртыванием, которое терпит обе версии во время перехода.

Практика

Посчитайте запросы, которые выдаёт обход

Возьмите список, где для каждого элемента показывается связанное значение, и посчитайте, сколько инструкций выдаёт отображатель.

Готово, когда: вы можете сказать, растёт ли число с количеством элементов, и указать сгенерированную инструкцию, которая повторилась.

Перенесите сброс внутрь его транзакции

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

Готово, когда: фиксация транзакции сохраняет ровно задуманные изменения, а сбой до фиксации не сохраняет ничего.

Частые ошибки

  • Обход связи внутри цикла — код читается как доступ к свойству и выдаёт запрос на итерацию; исправить: загружайте связь вместе с исходным запросом.
  • Предположение, что запись происходит на вызвавшей её строке — сброс случается на границе, возможно вне транзакции; исправить: знайте точку сброса и размещайте её осознанно.
  • Отсутствие взгляда на сгенерированную инструкцию — абстракции доверяют вслепую; исправить: включайте журналирование инструкций при разработке и просматривайте его.
  • Слишком долго живущая сессия — карта идентичности растёт, устаревшие объекты переиспользуются; исправить: ограничьте сессию одним запросом или операцией.
  • Отношение к модели отображателя как к схеме — нужна написанная вручную миграция, а модель устарела; исправить: держите миграции явными, а модель — в согласии с ними.

Что читать дальше