Комментарии 15
const users = await db.query('SELECT * FROM users');
// users уже массив объектовNode.js драйвер сразу возвращает объекты. И ORM сразу теряет смысл
Да, и в Java не факт, что ORM всегда нужен. Есть гораздо более легковесные решения, например, для некоторых наших проектов JdbcClient на порядок удобнее и проще, чем Hibernate
Ну prisma это удобно, видишь схему и он сам готовит миграции
Ну вроде да - местами удобно. Но такие решения часто сфокусированы на определенном виде удобства. CRUD - удобно, миграция к новой версии - вроде удобно, откат к старой - уже не понято. Сделать отчет выкачав данные из нескольких таблиц с предварительным расчетом агрегатов - не понятно. Миграции на knex - считай, чистый SQL с явным описанием как провести откат - вроде тоже удобно и прозрачно. Знание SQL кажется куда как более универсальным навыком, чем умение писать призма-схемы
Как по мне ORM начинает сиять когда надо делать вставку связанных таблиц, например у нас таблица и 20 связанных таблиц с отношением один ко многим с одним и более уровнем вложенности и я хочу сделать вставку всего этого добра. В ORM-е это делается за одну строчку, а теперь попробуйте сделать это с использованием билдера. Тоже самое касается и обновления и удаления. Почему-то в контексте ORM все говорят в основном только о SELECT-е игнорируя остальные операции.
Наверное так, но это кажется пугающим.
Как-то пришлось портировать проект с Java + Hibernate на Node. Сначала думали просто портировать, а потом полезли всякие странности в БД - какие-то гигантские ID при небольшом количестве записей, непонятные таблицы связей, ссылки на разные, но одинаково заполненные записи, большущие VIEW для каких-то промежуточных срезов. Ну вот это оказалось последствиями таких вот ORM-вставок и попыток распутать это пост-фактум. Мы тогда разделили API - связанные записи писали отдельно. На GUI стало возможным это отображать - "записываем это...", "обновляем то...", "готово!"
Правда связей было сильно меньше, чем 20. И вышло, конечно, многословнее. Но работать стало сильно быстрее, а из-за GUI казалось, что мгновенно.
Но самое главное - гораздо легче дебажить и апгрейдить (собственно, это была основная причина портирования)
НО! Не Java и не ORM были там главной причиной тормозов - и на Гибернейте можно было сделать удовлетворительно. Ну а на Ноде вообще отпала необходимость использовать ORM.
Желаю, конечно, чтобы у вас был другой случай, и чтобы всё было уместно.
Мы тогда разделили API - связанные записи писали отдельно.
Не всегда такое возможно чаще наоборот приезжает толстый JSON/XML в котором сразу и основная запись и связанные и надо все это записать единой логической операцией. Иногда и массив из основных записей плюс все сопутствующие. На моей памяти такое было довольно часто.
Видимо от оргструктуры зависит. Наша команда делала и клиента и сервер. Могли себе позволить. Сначала добавили новые ендпоинты с разделённым API, типа api/v2/... Потом клиент постепенно перешёл на новые запросы (пришлось частично переделывать логику), потом погасили старые. Потом откорректировали БД, т.к. с новым API структуру стало возможным упростить.
Автор, вместо тысячи слов приведу пример кода, который позволяет жить всем дружно и не упарываться на одной технологии. Код схематичный.
// Описываем сущности доменной области
type UserCreate = {
name: string;
}
type User = {
id: string;
name: string;
}
// Описываем интерфейсы и сервисы бизнес логики.
// На этом уровне они никак не привязаны к фреймворкам и базам
interface UserStorage {
createUser(user: UserCreate): Promise<string>;
getUser(id: string): Promise<User|null>
}
interface NotificationService {
sendMessage(userId: string, message: string): Promise<void>;
}
// Бизнес логика по управлению польтзователям
// Обратите внимание, логика уже работает, но разработчик сервиса понятия не имеет как дальше будет реализовано хранение и нотификация
// читай - масштабирование разработки (один пишет логику, второй уже заморачивается инфраструктурой)
class UserService {
private userRepository: UserStorage;
private notificationService: NotificationService;
constructor(userRepository: UserStorage, notificationService: NotificationService) {
this.userRepository = userRepository;
this.notificationService = notificationService;
}
async registerUser(userCreate: UserCreate) {
const userId = await this.userRepository.createUser(userCreate);
this.notificationService.sendMessage(userId, 'Вам зарегистрировали аккаунт')
}
async showUserProfile(userId: string) {
const user = await this.userRepository.getUser(userId);
console.log('Провиль пользователя: ', user);
}
}
class ConsoleNotificationService implements NotificationService {
async sendMessage(userId: string, message: string) {
console.log(`Сообщение пользователю ${userId}: ${message}`)
}
}
// Приступаем к реализации инфраструктуры.
// Тут уже появляется конкретика (инфраструктурный слой)
// Реализуем ORM инфраструктуру
@Entity()
export class OrmUser extends BaseEntity {
@PrimaryGeneratedColumn()
id: string;
@Column()
name: string;
}
// Реализуем конктерный UserStorage на основе ORM
class OrmUserStorage implements UserStorage{
async createUser(userCreate: UserCreate) {
const ormUser = this.mapUserCreateToOrmUser(userCreate);
await ormUser.save();
return ormUser.id;
}
async getUser(id: string) {
const ormUser = await OrmUser.findOneBy({id});
return this.mapOrmUserToUser(ormUser);
}
private mapUserCreateToOrmUser(createUser: UserCreate) {
const ormUser = new OrmUser()
ormUser.name = createUser.name;
return ormUser;
}
private mapOrmUserToUser(ormUser: OrmUser): User {
return {
id: ormUser.id,
name: ormUser.name
}
}
}
// Реализуем SQL репозиторий, на случай если придет стильный модный молодежный бунтарь Вася, и скажет что ORM это сакс
class BlazingFastSqlPostgrsqlUserStorage implements UserStorage {
async createUser(userCreate: UserCreate) {
const rows = await db.query(`insert to users (name) values (${userCreate.name})`);
return rows[0] as string;
}
async getUser(id: string) {
const rows = await db.query(`select * from user where id="${id}"`)
return rows[0] as User;
}
}
// Собираем приложение выбирая нужные реализации
function getNotificationsService(): NotificationService {
return new ConsoleNotificationService();
}
function getUserStorage(zoomerMode: boolean): UserStorage {
if (zoomerMode) {
return new BlazingFastSqlPostgrsqlUserStorage();
}
return new OrmUserStorage();
}
function getApp(zoomerMode: boolean) {
const notificationService = getNotificationsService();
const userStorage = getUserStorage(zoomerMode);
const userService = new UserService(userStorage, notificationService);
return {
userService,
}
}
// Конфигурируем окружение
const zoomerMode = getSkuffCount() < 3;
// Формируем контейнер зависимостей
const deps = getApp(zoomerMode);
// Запускаем роутинг
const app = express();
app.post('/users/create', async (req, res) => {
const userId = await deps.userService.registerUser({name: req.params.name});
res.send(userId)
})
app.get('/user/profile/:userId', async (req, res) => {
await deps.userService.showUserProfile(req.params.userId);
res.send('Смотри профиль в логах')
})
app.listen();По поводу самой статьи. Орм позволяет делать быстрые прототипы и одноразовые проекты - глупо упарываться с голым sql, потому что он позволяет как-то более менее синхронизировать структуру кода и базы, когда у тебя идет постоянный рефакторинг схемы хранения. Ну и в целом большинство операций - это круд в той или иной форме, где ты просто неизбежно сам будешь создавать орм.
Если у тебя проект долгоиграющий, то, возможно, есть смысл в оптимизации запросов. Но опять же, а в ОРМли дело? Может быть в принципе база большая и таблицы не оптимальные, а может быть вообще стоить добавить какой нить эластик сёрч или кликхаус?
В общем, такие заявления - голословны. Орм не на пустом месте появился и причины появления такого класса библиотек очень весомые. Я сам не раз видел как адепты sql неделями возились со своими запросам и как челики на орм делали это за один день. Нужно уметь выбирать нужный вариант, ведь основная задача инженера - это искать компромиссы.
Спасибо за комментарий!
Да, ORM действительно удобен для быстрых прототипов и типовых CRUD-операций. Но «быстрый прототип на SQL» может быть ещё проще: SELECT * и вставка через query builder, без абстракций. А когда появляется время — DDL легко превращается в типизированные интерфейсы (в том числе с помощью ИИ), и получается почти то же, что вы показали, но без ORM-слоя.
ORM появился не в экосистеме Node.js и не под REST-архитектуру — его сильные стороны проявляются в других контекстах. В лёгких сервисах часто проще и надёжнее писать SQL напрямую.
Я не отрицаю полезность ORM, но хочу подчеркнуть: сложные запросы он не заменяет, а простые — да, упрощает. При этом SQL универсален: он работает везде — от встраиваемых баз до аналитики и миграций. Поэтому вкладываться в понимание SQL продуктивнее, чем в изучение ORM-специфики.
А зачем вам вообще sql запросы писать? Почему вы не сделает DB as API - где наружу из базы данных только хранимые процедуры торчат, а sql не используется в принципе
DB as API не давали админы делать (корпоративные системы). Запись/апдейты через хранимки видел, но как ни странно, на джаве да еще и с гибернейтом. То что мы делали в рамках того проекта, разбивая на микро-REST-сервисы получалось сильно проще и не требовало хранимок (т.е. получался бы просто UPDATE с pl/sql обвесом). Один раз пробовали через вью и триггеры INSTEAD OF (view из нескольких таблиц) - считай то же что хранимки. Показалось чересчур неявно т.к. отдельного DBA не было, то все быстро забывали что от куда берётся. Кажется, что тут вступает закон Конвея - "структура ПО соответствует структуре команды". Если есть отдельный базовик, то через хранимки быстрее разрабатывать. Если отдельного нет, то быстрее, когда SQL в коде. Так-то получается-то что SQL все равно писать, вопрос только кто это будет делать и где.
Я уже джва года жду такого единомышленника

ORM в Node.js — когда от него больше вреда чем пользы. Почему вам, не нужен дополнительный абстрактный слой