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.
Комментарии (10)

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 кажется куда как более универсальным навыком, чем умение писать призма-схемы

TrueRomanus
29.10.2025 06:23Как по мне ORM начинает сиять когда надо делать вставку связанных таблиц, например у нас таблица и 20 связанных таблиц с отношением один ко многим с одним и более уровнем вложенности и я хочу сделать вставку всего этого добра. В ORM-е это делается за одну строчку, а теперь попробуйте сделать это с использованием билдера. Тоже самое касается и обновления и удаления. Почему-то в контексте ORM все говорят в основном только о SELECT-е игнорируя остальные операции.

mkant Автор
29.10.2025 06:23Наверное так, но это кажется пугающим.
Как-то пришлось портировать проект с Java + Hibernate на Node. Сначала думали просто портировать, а потом полезли всякие странности в БД - какие-то гигантские ID при небольшом количестве записей, непонятные таблицы связей, ссылки на разные, но одинаково заполненные записи, большущие VIEW для каких-то промежуточных срезов. Ну вот это оказалось последствиями таких вот ORM-вставок и попыток распутать это пост-фактум. Мы тогда разделили API - связанные записи писали отдельно. На GUI стало возможным это отображать - "записываем это...", "обновляем то...", "готово!"
Правда связей было сильно меньше, чем 20. И вышло, конечно, многословнее. Но работать стало сильно быстрее, а из-за GUI казалось, что мгновенно.
Но самое главное - гораздо легче дебажить и апгрейдить (собственно, это была основная причина портирования)
НО! Не Java и не ORM были там главной причиной тормозов - и на Гибернейте можно было сделать удовлетворительно. Ну а на Ноде вообще отпала необходимость использовать ORM.
Желаю, конечно, чтобы у вас был другой случай, и чтобы всё было уместно.
TrueRomanus
29.10.2025 06:23Мы тогда разделили API - связанные записи писали отдельно.
Не всегда такое возможно чаще наоборот приезжает толстый JSON/XML в котором сразу и основная запись и связанные и надо все это записать единой логической операцией. Иногда и массив из основных записей плюс все сопутствующие. На моей памяти такое было довольно часто.

mkant Автор
29.10.2025 06:23Видимо от оргструктуры зависит. Наша команда делала и клиента и сервер. Могли себе позволить. Сначала добавили новые ендпоинты с разделённым API, типа api/v2/... Потом клиент постепенно перешёл на новые запросы (пришлось частично переделывать логику), потом погасили старые. Потом откорректировали БД, т.к. с новым API структуру стало возможным упростить.
Dhwtj
Node.js драйвер сразу возвращает объекты. И ORM сразу теряет смысл
mkant Автор
Да, да. И что важно, node-драйвера из-за этого работают несколько медленнее - т.е. они тратят доп. ресурсы, чтобы вернуть нативные объекты вместо итератора. И поверх этого еще одну жирную абстракцию навертеть - это какая-то безудержная расточительность