Drizzle ORM: типобезопасный SQL без магии тяжёлых ORM
Привет, я Сергей Маркизов, бэкенд-разработчик в веб-продакшне Далее. В своих проектах я часто использую Drizzle ORM — инструмент для TypeScript-разработчиков, которым нужны строгие типы, но не хочется прятать SQL за несколькими слоями абстракций.
Классические ORM избавляют от шаблонного кода, однако на сложных проектах их удобство иногда превращается в ограничение. Появляются скрытое поведение, громоздкий API и сложности с нестандартными запросами. А при переходе на QueryBuilder или raw SQL часть преимуществ ORM может потеряться.
Drizzle предлагает компромисс: запросы остаются похожими на SQL, а TypeScript проверяет их на основе схемы базы данных.
Схема — это TypeScript-код
Например, так можно описать таблицу пользователей:
const activeUsers = await db
.select({
id: users.id,
email: users.email,
})
.from(users)
.where(eq(users.isActive, true))
.orderBy(desc(users.createdAt))
.limit(10);
Из этой схемы Drizzle выводит типы для чтения и записи. Отдельно поддерживать интерфейс пользователя и следить, чтобы он не расходился со структурой таблицы, не требуется:
type NewUser = typeof users.$inferInsert;
const user: NewUser = {
email: 'user@example.com',
};
Если передать поле неправильного типа или пропустить обязательное значение, TypeScript сообщит об ошибке ещё до запуска приложения.
Запросы остаются похожими на SQL
Для простых операций можно использовать ORM API, а более сложные запросы собирать через типизированный конструктор:
const activeUsers = await db
.select({
id: users.id,
email: users.email,
})
.from(users)
.where(eq(users.isActive, true))
.orderBy(desc(users.createdAt))
.limit(10);
Здесь нет отдельного языка запросов, который нужно мысленно переводить в SQL. select, from, where и orderBy остаются на своих местах, при этом поля и результат запроса типизированы.
Если возможностей конструктора недостаточно, можно перейти к SQL-фрагментам:
const result = await db
.select({
total: sql<number>`count(*)`.mapWith(Number),
})
.from(users)
Это удобно для агрегатов, CTE, специфичных для конкретной СУБД функций и других случаев, когда бороться с абстракцией сложнее, чем явно описать запрос.
Когда Drizzle особенно полезен
На мой взгляд, инструмент хорошо подходит разработчикам и командам, которые:
пишут на TypeScript и работают с PostgreSQL, MySQL или SQLite;
знают SQL и хотят контролировать реальные запросы;
сталкиваются с ограничениями Prisma или TypeORM;
не хотят дублировать описание таблиц и TypeScript-типы;
планируют внедрять ORM постепенно, без перестройки всей архитектуры.
Drizzle не привязан к конкретному фреймворку. Его можно использовать с Next.js, NestJS, Remix и другими TypeScript-решениями.
Но «магии» здесь действительно меньше
Это одновременно преимущество и ограничение Drizzle. ORM не скрывает работу с базой и не берёт на себя всю инфраструктуру.
Миграции нужно отдельно встроить в CI/CD, read/write split для реплик — реализовать на уровне приложения. Drizzle работает только с SQL-базами, а его экосистема пока меньше, чем у более зрелых ORM. Проект активно развивается, поэтому документация иногда не успевает за изменениями API.
Если команда рассчитывает почти не соприкасаться с SQL, такой подход может показаться слишком низкоуровневым.
Drizzle — это не ORM для тех, кто хочет забыть об SQL
Скорее, это типизированный инструментарий для разработчиков, которые SQL знают, хотят сохранить над ним контроль, но при этом использовать возможности TypeScript.
В полной версии статьи я подробнее разобрал описание связей, миграции, CRUD-операции, CTE, транзакции, оператор sql, расширения и ограничения Drizzle ORM.
Буду рад почитать о вашем опыте работы с Drizzle в комментариях.