Object-Relational Mapping (ORM) — технология, призванная «поженить» реляционную природу SQL-баз (PostgreSQL, MySQL, SQLite и т.п.) с объектной моделью языков программирования. Она настолько популярна, что её пытаются реализовать даже в необъектных языках — например, в Go или Erlang.
Если в Java без ORM действительно неудобно, то в экосистеме Node.js (и TypeScript в частности) ситуация принципиально иная. И ORM здесь — зачастую избыточная абстракция. В большинстве случаев рациональнее обойтись компактным SQL-билдером который сильно упрощает построение запросов, оставляя над ними полный контроль, и который совсем не занимается управлением объектами.
Disclaimer
Конечно, и в Node.js бывают случаи когда ORM необходим (они описаны ниже). Так же не надо искать повод убрать ORM из существующего проекта (игра не стоит свеч). Но вот, прежде чем добавлять его в новый проект, я бы несколько раз подумал, так как это чужая парадигма из другого языка.
Почему в Node.js ORM почти не даёт преимуществ
Большинство драйверов баз данных для JavaScript/TypeScript (в отличии от других языков, типа Java или Go) сразу возвращают массив нативных объектов, без каких-либо предупреждений или промежуточных слоёв:
const users = await db.query('SELECT id, name FROM users');
// users — это просто массив объектов: [{ id: 1, name: 'Анна' }, ...]
Типизация в TypeScript решается дженериками:
const users = await db.query<Users>('SELECT id, name FROM users');
Или одним ключевым словом as (чего не добиться в языках с номинальной типизацией, типа Java):
const users = (await db.query('SELECT id, name FROM users')) as User[];
В типичном сценарии Node.js — REST API — данные из базы почти сразу сериализуются в JSON и уходят клиенту. Никаких сложных манипуляций с объектами, никаких долгих жизненных циклов сущностей (как в языках общего назначения, где API лишь один из немногих сценариев). Если вам всё же нужен настоящий экземпляр класса — его проще создать вручную, чем тащить за собой тяжёлый ORM.
Почему ORM так популярен в Java — и почему это не наш случай
Hibernate ORM стал стандартом в Java-мире по объективным причинам:
Принятая архитектура и фреймворки требуют наличия объектов созданных по строгим правилам (Enterprise Java Beans, Spring Beans) — с приватными полями, геттерами/сеттерами и конструктором без аргументов — без библиотек это порождает массу шаблонного кода. И в этих бинах не редко содержится богатая логика, что редко бывает в API серверах, где объекты часто предназначены только для сериализации.
-
Драйверы JDBC возвращают ResultSet — итератор над курсором, по которому нужно вручную итерироваться и «поштучно» извлекать колонки:
while (resSet.next()) { users.add(new User(resSet.getInt("id"), resSet.getString("name"))); }ORM избавляет от этой рутины, сильно уменьшая количество бойлерплейта.
В Node.js всё иначе:
Объекты — нативный тип данных.
Драйверы сразу отдают массивы.
Приложения — в основном stateless HTTP-серверы, а не «жонглёры объектами».
ORM в JavaScript — это порт чужой парадигмы, которая не решает наших проблем, но создаёт новые.
И, кстати, не надо думать, что это «бесплатно» — именно из-за этих особенностей node-драйвера работают медленнее и потребляют больше памяти (чем в Java/Go), что еще больше ставит под сомнение необходимость дополнительных слоёв.
Почему это хорошая новость
Производительность и размер сборки. ORM — это сложные, объёмные библиотеки. Они замедляют запуск приложения, увеличивают размер бандла и добавляют накладные расходы к каждому запросу. Без ORM — меньше зависимостей, быстрее сборка, выше производительность.
-
SQL — человекочитаемый и мощный. Простые SQL-запросы читаются почти как разговорный английский:
SELECT name FROM users WHERE family_name = 'Иванов' AND age > 18;Это язык с десятилетиями развития, широким применением (отчёты, аналитика, ETL) и хорошей стандартизацией. ORM же прячет SQL за собственным предметно-ориентированным языком (DSL), который не переносится между библиотеками, и мешает оптимизации
Вы всё равно не избежите SQL. Как только логика усложняется — JOIN’ы, оконные функции, CTE, индексы — вам понадобится знание SQL. ORM не спасает от этого, «абстракция от базы» в этом случае становится иллюзией.
Когда ORM может быть оправдан
Конечно существуют нишевые случаи, когда ORM удобен или даже необходим:
Динамическое управление схемой БД. Например, в ERP-системах, где администраторы через GUI создают новые сущности и связи. Это тот случай когда существование без ORM едва ли возможно.
-
Поддержка нескольких СУБД «в бою». Если сборки вашего продукта действительно работают одновременно на разных СУБД — ORM может помочь. Но SQL-билдеры могут сделать тоже самое, при этом:
весят в несколько раз меньше (на конец 2025 года по данным npm — Knex в 20+ раз компактнее, чем TypeORM)
дают полный контроль над SQL
-
Пакетные операции. Да, ORM упрощает массовые вставки. Но с билдером это делается так же просто:
await knex('users').insert(usersArray); -
Вложенные объекты «в один запрос». ORM действительно может собрать
userсordersза одно обращение. Но:это может вызвать взрывное размножение строк или породить множество отдельных запросов, по одному на каждого пользователя, создав "проблему N+1",
в большинстве случаев лучше разделить ответы:
/users/1и/users/1/orders, это позволит GUI загружать данные параллельно, что увеличит отзывчивость интерфейса«Плоские» DTO (
orderTotal,orderDate,userId,userName) как правило, легче читать и анализировать.-
Если всё таки непременно нужны вложенные объекты, то современные СУБД умеют создать вложенный JSON на чистом SQL
как сделать вложенные объекты на PostgreSQL
Создаём и заполняем таблицы

Юзеры и заказы. У одного юзера может быть сколько угодно заказов create schema test; -- делаем отдельную схему, чтобы не мусорить create table test.users( id serial primary key, name text not null ); create table test.orders( id serial primary key, user_id int references test.users(id), date timestamptz not null default now(), value int not null default 0 ); -- заполняем --- insert into test.users(name) values ('Ваня'),('Петя'),('Вова'),('Даша'),('Маша'); insert into test.orders(user_id, value) values (1,10),(1,11),(2,5),(3,3),(3,33),(5,5),(5,55);Выбираем заказы так, чтобы юзеры были вложенным объектом
SELECT o.*, -- все поля заказа to_json(u) AS user -- юзер как JSON FROM test.orders o LEFT JOIN test.users u ON u.id = o.user_id;в результате получим
[ { "id": 1, "user_id": 1, "date": "2025-10-23 13:18:28.103 +0300", "value": 10, "user": { "id": 1, "name": "Ваня" } }, { "id": 2, "user_id": 1, "date": "2025-10-23 13:18:28.103 +0300", "value": 11, "user": { "id": 1, "name": "Ваня" } }, ... ]Теперь выбираем юзеров,так чтобы заказы были массивом объектов
SELECT u.*, COALESCE( -- на случай отсутствия заказов json_agg( -- массив to_json(o) -- один заказ как JSON ORDER BY o.date ) FILTER (WHERE o.id IS NOT NULL), -- исключаем заказы типа {"id" : null,...}, они возникнут, когда у юзера нет заказов '[]' -- подставится, когда нет ни одного заказа (т.к. null-заказы мы отфильтровали) ) AS orders FROM test.users u LEFT JOIN test.orders o ON u.id = o.user_id GROUP BY u.id; -- Из-за JOIN каждый заказ будет отдельной строкой, поэтому "схлопываем" их по юзеруПолучаем:
[ { "id": 1, "name": "Ваня", "orders": [ { "id": 2, "user_id": 1, "date": "2025-10-23T13:18:28.103179+03:00", "value": 11 }, { "id": 1, "user_id": 1, "date": "2025-10-23T13:18:28.103179+03:00", "value": 10 } ] }, { "id": 2, "name": "Петя", "orders": [ { "id": 3, "user_id": 2, "date": "2025-10-23T13:18:28.103179+03:00", "value": 5 } ] }, { "id": 3, "name": "Вова", "orders": [ { "id": 5, "user_id": 3, "date": "2025-10-23T13:18:28.103179+03:00", "value": 33 }, { "id": 4, "user_id": 3, "date": "2025-10-23T13:18:28.103179+03:00", "value": 3 } ] }, { "id": 4, "name": "Даша", "orders": [] }, { "id": 5, "name": "Маша", "orders": [ { "id": 7, "user_id": 5, "date": "2025-10-23T13:18:28.103179+03:00", "value": 55 }, { "id": 6, "user_id": 5, "date": "2025-10-23T13:18:28.103179+03:00", "value": 5 } ] } ]В MySQL и MariaDB есть аналогичные функции, немного отличающиеся по синтаксису.
Это не очень удобные конструкции, поэтому мой выбор — разделение API, но если вы не используете ORM и тут вдруг понадобились вложенные объекты — то это не проблема.
Если нужны не все поля, то вместо
to_json()лучше использоватьjson_build_object()- но это более многословно
Когда SQL пишут отдельные специалисты. Если у вас есть DBA, который создаёт VIEW, функции и триггеры, а разработчики работают только с простыми таблицами — ORM может быть безвреден. Но он не добавляет ценности, просто не мешает.
Что применять вместо ORM
Для большинства задач в Node.js лучше использовать:
Лёгкий SQL-билдер — всё равно какой (Knex.js один из самых популярных). Они обеспечивают параметризацию, защиту от SQL-инъекций, динамические условия, простую пакетную вставку/обновление и поддержку нескольких диалектов.
Типизацию через интерфейсы и
as.-
Простые маппинг-функции, если нужны классы:
const toUser = (row: any): User => new User(row.id, row.name); const users = rows.map(toUser); Прямой SQL через
pg,mysql2и т.п.для простого чтения или одиночной вставки/обновления
Вывод
ORM — мощный инструмент, но не универсальный. В экосистеме Node.js он:
не решает ключевых проблем,
добавляет сложность и накладные расходы,
мешает прямому использованию SQL — языка, который проще, мощнее и универсальнее любой ORM-обёртки.
Перед тем как добавлять ORM в проект на Node.js, честно ответьте себе: какие конкретные преимущества он принесёт? Если ответ — «все так делают» или «вдруг пригодится» — то это не повод. Чаще всего вам хватит драйвера и пары строк на TypeScript.
Комментарии (6)

Ares_ekb
29.10.2025 06:23Да, и в Java не факт, что ORM всегда нужен. Есть гораздо более легковесные решения, например, для некоторых наших проектов JdbcClient на порядок удобнее и проще, чем Hibernate

mkant Автор
29.10.2025 06:23Да, я напарывался, когда надо было сделать запрос посложнее, Hibernate скорее тормозит чем помогает. Но в Java хоть какое-то рациональное объяснение есть. А в Node который специально проектировался для HTTP серверов это бессмысленно гораздо чаще

zartdinov
29.10.2025 06:23Ну prisma это удобно, видишь схему и он сам готовит миграции

mkant Автор
29.10.2025 06:23Ну вроде да - местами удобно. Но такие решения часто сфокусированы на определенном виде удобства. CRUD - удобно, миграция к новой версии - вроде удобно, откат к старой - уже не понято. Сделать отчет выкачав данные из нескольких таблиц с предварительным расчетом агрегатов - не понятно. Миграции на knex - считай, чистый SQL с явным описанием как провести откат - вроде тоже удобно и прозрачно. Знание SQL кажется куда как более универсальным навыком, чем умение писать призма-схемы
Dhwtj
Node.js драйвер сразу возвращает объекты. И ORM сразу теряет смысл
mkant Автор
Да, да. И что важно, node-драйвера из-за этого работают несколько медленнее - т.е. они тратят доп. ресурсы, чтобы вернуть нативные объекты вместо итератора. И поверх этого еще одну жирную абстракцию навертеть - это какая-то безудержная расточительность