Обновить

Комментарии 15

const users = await db.query('SELECT * FROM users');
// users уже массив объектов

Node.js драйвер сразу возвращает объекты. И ORM сразу теряет смысл

Да, да. И что важно, node-драйвера из-за этого работают несколько медленнее - т.е. они тратят доп. ресурсы, чтобы вернуть нативные объекты вместо итератора. И поверх этого еще одну жирную абстракцию навертеть - это какая-то безудержная расточительность

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

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

Ну 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 все равно писать, вопрос только кто это будет делать и где.

Я уже джва года жду такого единомышленника

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации