Prisma ORM
Prisma
Что это такое
Prisma — объектно-реляционное отображение для Node и TypeScript, где источник истины — схема. Один файл схемы описывает модели и их связи, из него инструмент генерирует типизированный клиент с методами под эту схему и миграции, приводящие базу данных в соответствие с ней, а запрос против клиента проверяется типами при компиляции и превращается в инструкцию во время выполнения.
Почему это важно
Главное достоинство — сгенерированные типы: запрос, который выбирает несуществующее поле или передаёт значение не того типа, становится ошибкой компиляции, а не сюрпризом во время выполнения, — для языка с необязательной системой типов это заметный шаг вперёд по сравнению с отображениями, которые находят такие ошибки уже в продакшне. Команда получает в файле схемы одно место, где искать модель данных, а сгенерированная история миграций делает изменение схемы артефактом, который можно отревьюить; цена же та же, что у любого отображения. Сгенерированная инструкция скрыта, обход связи по-прежнему может порождать запрос на каждый элемент, и вдобавок нужен этап генерации, который должен пройти до того, как код скомпилируется.
файл схемы -> сгенерированные типы + миграция -> типизированный клиент -> SQL
Ключевые идеи
Файл схемы — источник истины и для кода, и для базы данных
Модели, поля и связи объявляются один раз, всё остальное выводится: изменение схемы меняет сгенерированные типы и порождает миграцию, поэтому разойтись так, как расходятся модель, написанная руками, и база, правленная руками, они не могут. В этом и состоит центральное проектное решение; отсюда же и отличие рабочего процесса инструмента от отображений, которые читают базу данных.
Сгенерированные типы переносят ошибки на время компиляции
Редактор дополняет имена полей, поэтому заглядывать в схему при написании запроса приходится реже. Тип результата запроса выводится из схемы, включая форму включённых связей, а переименование поля ломает при сборке каждый запрос, который его использовал, — ровно ради такого поведения и существует статически типизированный язык.
const order = await db.order.findUnique({
where: { id },
include: { items: true },
});
// order.items типизирован; order.missing не скомпилируется
Клиент — это построитель запросов, и инструкция всё ещё генерируется
Запрос собирается из вызовов методов, которые читаются как желаемый результат, а не как SQL, который выполнится. Читаемость от этого выигрывает, однако центральная утечка отображения сохраняется: обернётся ли обход соединением, вторым запросом или запросом на каждую строку, зависит от того, как написан запрос, и узнать это надёжно можно единственным способом — включить журналирование сгенерированных инструкций и прочитать их.
Миграции генерируются, а затем ревьюятся
Инструмент умеет вывести миграцию из изменения схемы, и выведенную инструкцию следует прочитать до применения — особенно там, где речь о переименовании, которое инструмент может выразить как удаление и создание. Потерю данных при обычном изменении предотвращает именно отношение к сгенерированной миграции как к коду, который можно ревьюить, а не как к готовому артефакту.
Клиент нужно генерировать и держать в актуальном состоянии
Типизированный клиент создаётся этапом генерации, поэтому сборка, пропустившая его, компилируется против устаревшего клиента или падает вовсе, и место этому — в стандартных скриптах проекта и в непрерывной интеграции, а не в заметке в readme.
Работа с соединениями и транзакции заданы явно
Клиент управляет пулом соединений, а группу операций можно выполнить в транзакции, вызвав её. Отображение ничего не буферизует бессрочно, поэтому граница транзакции записана там, где говорит код; по сравнению с отображениями, у которых точка сброса неявна, это настоящее улучшение.
Практика
Проследите связь от сгенерированного типа
Напишите запрос, который возвращает запись вместе с одной из её связей, и определите по типу и по журналу инструкций, была ли связь получена в той же инструкции.
Готово, когда: вы можете процитировать сгенерированную инструкцию и сказать, была выдана одна или две.
Отревьюйте сгенерированную миграцию
Переименуйте поле в схеме, сгенерируйте миграцию и прочитайте, что она сделает с уже имеющимися данными.
Готово, когда: вы можете сказать, сохранило ли изменение данные столбца или удалило и создало его заново, и скорректировали миграцию, если нет.
Частые ошибки
- Пропуск этапа генерации при сборке — клиент устарел или сборка падает; исправить: поставьте генерацию в стандартные скрипты и в непрерывную интеграцию.
- Применение сгенерированной миграции без её чтения — переименование может быть выражено удалением и созданием; исправить: ревьюьте каждую миграцию как код.
- Включение связей в цикле — сгенерированные инструкции размножаются вместе с размером результата; исправить: включите связь в исходный запрос или выполняйте пакетно.
- Предположение, что один файл схемы меняет базу данных — миграцию нужно применить; исправить: считайте изменение схемы и миграцию одним изменением.
- Оставление транзакций на усмотрение отображения — группа записей, которая должна быть атомарной, написана как отдельные вызовы; исправить: задайте границу транзакции явно.
Что читать дальше
- Объектно-реляционное отображение (05-backend-data/15-object-relational-mapping) — абстракция и её утечки, поэтому читайте её первой.
- Node.js (05-backend-data/02-nodejs) — среда выполнения, в которой работает этот клиент.
- SQL и реляционные базы данных (05-backend-data/07-sql-relational-databases) — инструкции, которые генерирует клиент.