ORM в Node.js: Sequelize, Prisma и когда ORM не нужен

Содержание

О проверке кода. Примеры не запускались на живой базе — в отличие от MongoDB и Redis, где был фактический вывод. Код дан по документации Sequelize и Prisma. Говорим прямо, а не ставим бейдж «проверено».

ORM обещает избавить от SQL. Разберём, что это на самом деле даёт, где ломается и почему выбор инструмента за девять лет сменился.

Что такое ORM

ORM (Object-Relational Mapping) — слой между вашими объектами и таблицами базы. Строка таблицы становится объектом, методы объекта превращаются в SQL.

// без ORM — сами пишем SQL
const [rows] = await pool.query('SELECT * FROM users WHERE id = ?', [42]);

// с ORM — работаем с объектами
const user = await User.findByPk(42);
user.name = 'Борис';
await user.save();   // ORM сгенерирует UPDATE

Идея привлекательная: думаешь объектами, а не таблицами. Проблема в том, что объекты и таблицы устроены по-разному, и абстракция протекает ровно там, где это неудобнее всего.

Sequelize: как это выглядит

import { Sequelize, DataTypes } from 'sequelize';

const sequelize = new Sequelize('sqlite::memory:');

const User = sequelize.define('User', {
  name: DataTypes.STRING,
  age: DataTypes.INTEGER,
});

await sequelize.sync();
await User.create({ name: 'Аня', age: 30 });
const adults = await User.findAll({ where: { age: { [Op.gte]: 18 } } });

Схема описывается моделью, запросы — методами. Пока задачи простые — CRUD, фильтры, — это читается лучше SQL.

Проблема N+1: главная ловушка ORM

Самая частая причина, по которой ORM-приложение внезапно тормозит. Выглядит безобидно:

// ❌ N+1: один запрос на список + по одному на каждого
const users = await User.findAll();          // 1 запрос
for (const user of users) {
  const posts = await user.getPosts();        // +1 запрос НА КАЖДОГО
}
// 100 пользователей = 101 запрос к базе

Код чистый, логика очевидная — а база получает сто один запрос вместо одного. На разработке с десятью записями незаметно; в проде на тысячах — стена.

// ✅ жадная загрузка: один запрос с JOIN
const users = await User.findAll({ include: Post });

N+1 — это оборотная сторона удобства ORM. Он так хорошо прячет SQL, что вы не видите, сколько запросов на самом деле уходит. Лечится жадной загрузкой (include), но сначала проблему надо заметить — а для этого нужно смотреть на реальные запросы, как explain в MongoDB показывает COLLSCAN.

Дырявая абстракция

Здесь работает тот же урок, что и с Waterline в Sails: абстракция над базой протекает на сложных запросах.

ORM или сырой SQL: где граница

ORM на своём месте

Простые операции

  • CRUD: создать, найти по id, обновить
  • Фильтры по полям
  • Связи один-ко-многим с include
  • Миграции схемы
  • Защита от инъекций из коробки

Честнее сырой SQL

Сложные запросы

  • Аналитика, агрегации, GROUP BY
  • Оконные функции, CTE
  • Хитрые многотабличные JOIN
  • Тонкая оптимизация под план запроса
  • ORM тут либо тормозит, либо не выражает

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

Безопасность: работает, пока не обходите

ORM экранирует параметры автоматически — в этом его плюс:

// ✅ безопасно: ORM подставит значение как параметр
User.findAll({ where: { email: userInput } });

Но защита держится ровно до сырого SQL со строковой подстановкой:

// ❌ дыра, даже внутри ORM
sequelize.query(`SELECT * FROM users WHERE email = '${userInput}'`);

// ✅ и здесь параметры
sequelize.query('SELECT * FROM users WHERE email = ?', { replacements: [userInput] });

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

Что выбрать в 2026

Инструментов стало больше, и Sequelize — уже не единственный ответ:

Инструмент Чем берёт
Prisma типобезопасность, генерируемый клиент, современный API
Drizzle лёгкий, ближе к SQL, отличная типизация
Sequelize зрелый, много кода в проде, но воспринимается как наследие
Kysely не ORM, а типобезопасный конструктор запросов
чистый драйвер mysql2, полный контроль

Ключевой сдвиг с 2017 года — типобезопасность. Prisma и Drizzle не просто генерируют SQL, а дают типы: опечатка в имени поля становится ошибкой на этапе сборки, а не багом в проде. Это и есть главная причина, по которой новые проекты уходят от Sequelize.

И честная альтернатива всему: для простого приложения драйвера mysql2 достаточно. SQL вы пишете сами — но видите каждый запрос и не ловите N+1 внезапно.

Что было на этой странице в 2017 году

Первая версия вышла 29 июля 2017 года и учила Sequelize как основной способ работать с реляционной базой из Node. Для 2017 года — верно: альтернатив зрелого уровня почти не было, а писать SQL руками считалось прошлым веком.

Что изменилось:

  • Sequelize из стандарта стал одним из вариантов — и не первым. Появились Prisma и Drizzle с типобезопасностью, которой в 2017-м просто не существовало.
  • Отношение к сырому SQL изменилось. Тогда его избегали; сегодня понятно, что на сложных запросах он честнее ORM, а не «примитивнее».
  • N+1 и дырявость абстракции в статье не упоминались вовсе — а это ровно то, обо что спотыкаются в реальных проектах.

Мы переписали урок не как «руководство по Sequelize», а как разговор о том, что такое ORM, где он помогает и где мешает — потому что сегодня главный вопрос не «как настроить Sequelize», а «нужен ли ORM вообще для этой задачи».


Смежные темы: MySQL напрямую — жизнь без ORM; что такое MongoDB — когда база вообще не реляционная; Sails.js — про дырявость абстракции. Полный список — в справочнике по Node.js.

Частые вопросы

Что такое ORM простыми словами?
Слой, который превращает строки таблицы в объекты вашего кода и обратно. Вы пишете user.save() вместо INSERT INTO users, а ORM генерирует SQL. Удобно, пока запросы простые.
Sequelize или Prisma в 2026?
Для нового проекта чаще Prisma или Drizzle: типобезопасность, современный API, активная разработка. Sequelize живой и рабочий, но воспринимается как наследие. Для существующего кода на Sequelize мигрировать без причины не нужно.
Что такое проблема N+1?
Когда вместо одного запроса ORM делает N+1: один на список и по одному на каждую связанную запись. Сто пользователей с адресами — сто один запрос вместо одного с JOIN. Главная скрытая причина медленных ORM-приложений.
ORM защищает от SQL-инъекций?
Да, если пользоваться им правильно: параметры экранируются автоматически. Но как только вы вставляете сырой SQL со строковой подстановкой в обход ORM — защита пропадает.
Когда лучше писать чистый SQL?
Когда запрос сложный: аналитика, оконные функции, хитрые JOIN. ORM на них либо генерирует неоптимальный SQL, либо не выражает их вовсе, и вы всё равно пишете сырой запрос — только через прослойку.