Раздел 8 · Frameworks и Libs

Карта ORM: TypeORM, Drizzle, Kysely, Prisma

middle~35 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Карта ORM: TypeORM, Drizzle, Kysely, Prisma

Фреймворк отвечает за то, как приходит запрос и уходит ответ. Но между ними почти всегда лежит база, и работать с ней руками через сырые строки SQL в боевом коде неудобно: легко ошибиться в имени столбца, легко открыть SQL-инъекцию, тяжело держать типы в синхроне со схемой. Этот слой закрывает ORM или query builder. Урок про то, чем четыре популярных инструмента отличаются и как каждый ложится на Fastify и на Nest.

Важно сразу снять ложную дихотомию. Вопрос не «какой ORM лучший», а «сколько абстракции над SQL тебе нужно и чем ты за неё платишь». Чем выше абстракция, тем меньше думаешь про SQL и тем дальше уходишь от него, когда запрос становится сложным. Разложим инструменты по этой оси.

1. Где живёт слой данных

И в Fastify, и в Nest слой данных, это отдельный объект, а не код прямо в обработчике. Разница только в том, как его подключают.

  • Fastify: репозиторий, это сервис в awilix-контейнере (урок 05), он живёт в плагине и резолвится per-request scope. Обработчик роута достаёт его из контейнера и зовёт метод.
  • Nest: репозиторий, это провайдер в модуле (уроки 06-07), он приходит в конструктор сервиса через DI-контейнер.

ORM в обоих случаях, это деталь внутри репозитория. Меняя ORM, ты меняешь тело методов репозитория, а не контракт, которым пользуется остальное приложение. Это ровно тот шов, ради которого мы держим репозиторий отдельно: репозиторий изолирует выбор ORM от бизнес-логики.

2. Две модели: Data Mapper против Active Record

Прежде чем смотреть инструменты, нужно понять две классические модели, между которыми они выбирают.

  • Active Record: сущность сама умеет себя сохранять. У объекта article есть метод article.save(), запись знает про базу. Просто на старте, но мешает тесты и доменную логику: объект тащит зависимость от хранилища.
  • Data Mapper: сущность, это просто данные, а сохранением занимается отдельный объект (репозиторий или менеджер). Чище для домена, чуть больше кода.

TypeORM поддерживает обе модели, по умолчанию рекомендует Data Mapper. Drizzle, Kysely и Prisma в эту дихотомию не играют вовсе: у них нет «сущности с поведением», есть таблицы и функции-запросы. Это уже следующее поколение, ближе к query builder, чем к классическому ORM.

3. TypeORM: классический ORM на декораторах

TypeORM, это зрелый Data-Mapper ORM, тесно сросшийся с Nest (в уроках 07-08 мы работали именно с ним). Сущность описывается классом с декораторами, репозиторий получаешь у DataSource.

@Entity()
class Article {
  @PrimaryGeneratedColumn('uuid') id!: string;
  @Column() title!: string;
  @Column() status!: 'draft' | 'published';
  @ManyToOne(() => User) author!: User;
  @CreateDateColumn() publishedAt!: Date;
}

// репозиторий и выборка
const repo = dataSource.getRepository(Article);
const articles = await repo.find({
  where: { author: { id: authorId }, status: 'published' },
  relations: { author: true },
  order: { publishedAt: 'DESC' },
  take: 20,
});

Сильные стороны: богатый набор готового (relations, каскады, listeners, миграции, наследование таблиц), глубокая интеграция с Nest через @nestjs/typeorm. Слабые: типобезопасность дырявая. Опечатка в строковом relations или в условии всплывает в рантайме, а сложные запросы уходят в QueryBuilder, где типы почти теряются. Плюс цена декораторов и рефлексии, та же, что у Nest-контейнера (урок 09). Это рабочая лошадка больших монолитов, но не самый типобезопасный выбор в 2026.

4. Drizzle: typed query builder, выбран в BatSchool

Drizzle, это не классический ORM, а тонкий query builder поверх SQL. Таблицы объявляются прямо в TypeScript, и типы выводятся из этого объявления, отдельного шага кодогенерации нет.

export const articles = pgTable('articles', {
  id: uuid('id').primaryKey().defaultRandom(),
  title: text('title').notNull(),
  status: text('status').$type<'draft' | 'published'>().notNull(),
  authorId: uuid('author_id').notNull(),
  publishedAt: timestamp('published_at').notNull(),
});

const rows = await db
  .select()
  .from(articles)
  .where(and(eq(articles.authorId, authorId), eq(articles.status, 'published')))
  .leftJoin(users, eq(articles.authorId, users.id))
  .orderBy(desc(articles.publishedAt))
  .limit(20);

Сильные стороны: запрос читается почти как SQL, типы выводятся из схемы, опечатку в имени поля компилятор не пропустит, рантайм крошечный, без рефлексии. Именно поэтому Drizzle выбран и в самой BatSchool: схема и слой данных описаны ровно так. Слабые: меньше «магии» из коробки, чем у TypeORM или Prisma, часть обвязки (например, сложные связи) описываешь руками. Это лучший дефолт, когда хочешь близость к SQL и сильные типы без кодогена.

дальше после входа

Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.

Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.