Карта 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.