Привет, я Сергей Маркизов, разработчик диджитал-продакшна Далее. В наших проектах часто использую Drizzle — современную, типобезопасную ORM для TypeScript, которая не усложняет базовую задачу: читать и писать данные. В этой статье расскажу, чем библиотека отличается от других и как с ней работать.

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

Но, несмотря на все свои плюсы, ORM сталкиваются с постоянной критикой из-за своих минусов.

  1. Плохая производительность и неспособность использовать многие возможности конкретных баз данных.

  2. Большие размеры кодовой базы этих решений, как правило, приводят к долгоживущим багам, которые не исправляются годами. 

  3. Попытки сделать один инструмент для работы как с SQL, так и с NoSQL базами еще сильнее сужают полноценное использование возможностей СУБД.

Так, на проектах, где требуется построение сложных SQL-запросы, TypeORM быстро теряет все свои преимущества. Внутренние механизмы технологии дают большой оверхед и работают не всегда корректно — даже при использовании QueryBuilder. Более того, с применением этого конструктора увеличивается вероятность возникновения багов из-за отсутствия проверки типов. Особенно при запросах, результаты которых не укладываются в схему данных таблиц.

Все же не стоить хейтить все ORM, попробовав один-два инструмента. Существуют и достаточно легковесные решения для работы с БД. Один из них — Drizzle ORM.

Знакомство с Drizzle ORM и описание отличительных черт

Если вы устали от сложных ORM, которые прячут SQL за слоями абстракций, то вам стоит обратить внимание на Drizzle.

Это легковесный, типобезопасный и максимально близкий к SQL инструмент для работы с базами данных — с широкой поддержкой типизированных запросов с помощью TypeScript.

Drizzle позволяет строить SQL-запросы с проверкой типов на основе описания схемы базы данных и не имеет накладных расходов, присущих многим альтернативным инструментам для работы с БД.

Преимущества Drizzle в сравнении с другими ORM

Инструмент

Минусы в работе

Решения Drizzle ORM

Sequelize

Слабая поддержка TypeScript, устаревшая архитектура, дублирование описания схем и типов

В Drizzle типы формируются из схемы, описание структуры — в одном месте

TypeORM

Много скрытого поведения, нестабильная типизация, громоздкий API

В Drizzle все явно: схема — это код, запросы — читаемые, поведение — предсказуемое

Prisma

Высокий порог входа, генерация клиента, большой вес проекта

Drizzle не требует генерации, подключается за 2 строки, не увеличивает bundle

Knex

Нет встроенной типизации, всю логику нужно писать руками

Drizzle дает типы из коробки, но оставляет контроль над структурой и логикой

Поддерживаемые БД и создание соединения с базой

На текущий момент Drizzle поддерживает следующие SQL-СУБД: PostgreSQL, MySQL, SQLite.

Для создания соединения достаточно указать URL базы данных:

const connection = drizzle(process.env.DATABASE_URL);

Но для полноценного использования возможностей типизации TypeScript следует указать схему при создании соединения:

const connection = drizzle(process.env.DATABASE_URL!, {
  casing: 'snake_case', // В случае различий в регистре
  logger: true,         // Можно передать логгер или true для использования логгера по умолчанию
  schema: {
    ...schema,
    ...relations,
  },
});

Подробное описание схемы данных и ограничений

В основе описания структуры базы данных в Drizzle ORM лежат таблицы и отношения между ними:

const customBoolean = customType<{ data: boolean }>({
  dataType() {
    return 'boolean';
  },
});

export const examples = pgTable("examples", {
  id: uuid().primaryKey().defaultRandom(), // Первичный ключ
  createdAt: timestamp().defaultNow().notNull(), // Временные метки создания можно определить таким образом

  someVarchar: varchar({ length: 256 }).unique(),
  someInteger: integer().notNull(),
  someDecimal: decimal(),
  someBoolean: boolean().default(true),
  someArray: varchar({}).array(),
  someJson: json(),
  someJsonb: jsonb(),
  someText: text(),
  someTime: time(),
  someTimestamp: timestamp(),
  someUuid: uuid(),
  someCustomBoolean: customBoolean(),
}, table => [ // Индексы на несколько колонок можно добавить следующим образом
  index('examples_some_idx').on(table.someInteger, table.someTimestamp),
  unique('examples_unique').on(table.someBoolean, table.someText),
]);

One-to-one

export const players = pgTable('players', {
  id: uuid().primaryKey().defaultRandom(),
  firstName: varchar({ length: 255 }).notNull(),
  lastName: varchar({ length: 255 }).notNull(),
});

export const playerGameStates = pgTable('player_game_state', {
  id: uuid().primaryKey().defaultRandom(),
  playerId: uuid().notNull().references(() => players.id),
  metadata: jsonb().notNull(),
});

export const playersRelations = relations(players, ({ one }) => ({
  state: one(playerGameStates, {
    fields: [players.id],
    references: [playerGameStates.playerId],
  }),
}));

One-to-many

export const positions = pgTable('positions', {
  id: uuid().primaryKey().defaultRandom(),
  name: varchar({ length: 256 }).unique().notNull(),
  responsibilities: varchar({ length: 256 }).array().notNull(),
});

export const employees = pgTable('employees', {
  id: uuid().primaryKey().defaultRandom(),
  firstName: varchar({ length: 256 }).unique().notNull(),
  lastName: varchar({ length: 256 }).unique().notNull(),
  positionId: uuid().notNull().references(() => positions.id),
});

export const positionsRelations = relations(positions, ({ many }) => ({
  employees: many(employees),
}));

export const employeesRelations = relations(employees, ({ one }) => ({
  position: one(positions, {
    fields: [employees.positionId],
    references: [positions.id],
  }),
}));

Many-to-many

export const customers = pgTable('customers', {
  id: uuid().primaryKey().defaultRandom(),
  firstName: varchar({ length: 256 }).unique().notNull(),
  lastName: varchar({ length: 256 }).unique().notNull(),
});

export const services = pgTable('services', {
  id: uuid().primaryKey().defaultRandom(),
  name: varchar({ length: 256 }).unique().notNull(),
});

export const subscriptions = pgTable('subscriptions', {
  createdAt: timestamp().defaultNow().notNull(),
  isActive: boolean().notNull().default(true),
  customerId: uuid().notNull().references(() => customers.id),
  serviceId: uuid().notNull().references(() => services.id),
}, table => [
  primaryKey({ columns: [table.customerId, table.serviceId] }),
]);

export const customersToServicesSubscriptionRelation = relations(subscriptions, ({ one }) => ({
  customer: one(customers, {
    fields: [subscriptions.customerId],
    references: [customers.id],
  }),
  service: one(services, {
    fields: [subscriptions.serviceId],
    references: [services.id],
  }),
}));

Миграции и построение схемы на основе существующей базы

Для работы с миграциями в Drizzle ORM используется утилита drizzle-kit. Ее нужно установить отдельно.

npm install drizzle-kit

Набор команд:

npx drizzle-kit migrate # Применить миграции
npx drizzle-kit generate # Сгенерировать миграции на основе текущей схемы и структуры базы данных
npx drizzle-kit push # Актуализировать базу данных в обход механизма миграций (удобно при прототипировании)
npx drizzle-kit pull # Построить файлы схемы и отношений на основе существующей базы данных

Конфигурация инструмента описывается в файле drizzle.config.ts или drizzle.config.js.

import 'dotenv/config'; // Подгружаем переменные окружения
import { defineConfig } from 'drizzle-kit';

export default defineConfig({
  out: './drizzle', // Рабочая директория для миграций и генерации схемы
  schema: './src/drizzle/schema.ts', // Схема, которая будет использоваться для генерации миграций
  dialect: 'postgresql',
  casing: 'snake_case',
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
});

Создание запросов

Для валидации передаваемых значений используется TypeScript, для валидации схем данных — расширение drizzle-zod.

Извлечение данных через ORM-запросы

Извлечение данных в Drizzle ORM можно осуществлять без построения запросов вручную. Для этого в нем предусмотрено два метода: findMany и findFirst.

const accounts = await postgres.query.accounts.findMany({
    where: eq(schema.accounts.isActive, true),
    limit: 5,
    offset: 100,
    orderBy: [desc(schema.accounts.createdAt)],
});

const account = await postgres.query.accounts.findFirst({
    where: and(
        eq(schema.accounts.isActive, true),
        isNotNull(schema.accounts.email),
    ),
    with: {
        carts: true,
    },
});

Извлечение данных через оператор SELECT

Для извлечения данных можно воспользоваться встроенным API построения SQL-запросов. По синтаксису он почти не отличается от других query builder'ов, за исключением типизации, которая здесь строго выведена из схемы.

postgres.select().from(schema.stores)

postgres.select({
    id: schema.stores.id,
    title: schema.stores.title,    
})
.from(schema.stores)
.where(eq(schema.stores.isActive, true))

Джоины описываются таким же образом, как и в большинстве других конструкторов запросов.

await postgres.select()
.from(schema.customerCarts)
.innerJoin(schema.orders, and(
    eq(schema.orders.cartId, schema.customerCarts.id),
    gte(schema.orders.totalCost, 100),
))

Псевдонимы колонок и SQL-выражения можно задать с помощью встроенного в Drizzle ORM оператора sql. Остальные его возможности мы рассмотрим позже.

postgres.select({
  id: sql`${schema.accounts.id}`.as('someId'),
  fullName: sql`${schema.accounts.firstName} || ' ' || ${schema.accounts.lastName}	`.as('fullName'),
}).from(schema.accounts)

Подзапросы и CTE описываются схожим образом:

const subquery = postgres.select().from(schema.stores);

const rowsFromSubquery = await postgres.select().from(schema.stores);


const statsCTE = postgres.$with('stats').as(
    postgres.select({
        accountId:  sql`${schema.accounts.id}`.as('accountId'),
        storeId: sql`${schema.stores.id}`.as('storeId'),
        totalCost: sql`sum(${schema.orders.totalCost})`.as('totalCost'),
        count: sql`sum(${schema.customerCartProducts.count})`.as('count'),
    }).from(schema.stores)
    .innerJoin(schema.customerCarts, eq(schema.customerCarts.storeId, schema.stores.id))
    .innerJoin(schema.accounts, eq(schema.accounts.id, schema.customerCarts.accountId))
    .innerJoin(schema.customerCartProducts, eq(schema.customerCartProducts.cartId, schema.customerCarts.id))
    .innerJoin(schema.orders, eq(schema.orders.cartId, schema.customerCarts.id))
    .where(and(
        inArray(schema.stores.title, stores),
        eq(schema.customerCarts.wasOrdered, true)
    ))
    .groupBy(schema.stores.id, schema.accounts.id)
);

const rowsWithCTE = await postgres.with(statsCTE).select().from(schema.accounts).innerJoin(statsCTE, eq(statsCTE.accountId, schema.accounts.id))

Добавление данных

Drizzle ORM дает возможность проверки корректности типов данных при добавлении в базу — на основе описанной схемы данных. Эти типы можно явно указывать в коде, что позволяет избежать ошибок из-за недостающих данных при подобных операциях. Также можно указать поведение в случае конфликта.

// Сохраним тип черновика, для явного указания
type AccountDraft = typeof schema.accounts.$inferInsert;

const draft1: AccountDraft = {
    firstName: 'John',
    lastName: 'Doe',
};

const draft2: AccountDraft = {
    firstName: 'Robinson',
    lastName: 'Crusoe',
};

// const rows = await postgres.insert(schema.accounts).values(draft1).onConflictDoNothing({ target: schema.accounts.id }).returning();

const rows = await postgres.insert(schema.accounts).values([
    draft1,
    draft2
]).returning().onConflictDoUpdate({
    target: schema.accounts.id,
    set: { firstName: 'Julius', lastName: 'Caesar' },
});

Обновление данных

Обновление данных в Drizzle ORM максимально приближено к SQL.

await postgres.update(schema.stores).set({
    isActive: false,
}).where(
    gte(schema.stores.createdAt, new Date('2025-01-01'))
).returning();

Удаление данных

Удалять данные можно с помощью ранее указанных выражений.

await postgres.delete(schema.players).where(and(
    eq(schema.players.firstName, 'John'),
    eq(schema.players.lastName, 'Doe'),
)).returning();

Работа с оператором sql

При работе с ORM-библиотекой могут возникнуть ситуации, когда написать конкретный запрос с использованием синтаксиса ORM оказывается сложно.

В подобных случаях стоит прибегнуть к использованию параметризованных сырых запросов raw queries и/или преобразованию объектов из TypeScript к SQL-подобному синтаксису — для указания в параметрах. Для этого в Drizzle ORM существует специальный оператор sql. Ниже рассмотрим основные варианты его использования.

Шаблон sql

Простой пример использования оператора sql — подстановка в сырой SQL-запрос наименований таблиц и колонок, а также параметров:

await postgres.execute(
    sql`select ${schema.accounts.phone}, ${schema.accounts.email} from ${schema.accounts} where ${schema.accounts.id} = ${accountId}`
)

sql``.mapWith()

Для указания правил, по которым стоит преобразовывать те или иные данные, можно воспользоваться конструкцией:

postgres.select({
    id: schema.accounts.id,
    count: sql<number>`count(*)`.mapWith(Number) 
}).from(schema.accounts);

sql``.as()

Для того чтобы задать псевдоним для имени, можно взять следующую конструкцию:

sql`sum(store_products.cost)`.as('store_products_total_cost')

sql.raw()

Метод sql.raw() нужен тогда, когда требуется защита содержимого от экранирования или любых других преобразований.

Пример из документации:

sql`select * from ${usersTable} where id = ${12}`;
// select * from "users" where id = $1; --> [12]

sql.raw(`select * from "users" where id = ${12}`);
sql`select * from ${usersTable} where id = ${sql.raw(12)}`;
// select * from "users" where id = 12;

sql.join()

Метод sql.join() поможет объединить несколько параметризованных запросов. 

sql.join([
    sql`select id, first_name, last_name, 'account' from ${schema.accounts}`,
    sql`select id, first_name, last_name, 'employee' from ${schema.employees}`,
], sql.raw(' union '))

// select id, first_name, last_name, 'account' from "accounts" union select id, first_name, last_name, 'employee' from "employees"

Работа с транзакциями

Работа с транзакциями в Drizzle ORM реализована на основе вызова callback`а с получением объекта транзакции для осуществления операций с базо

await postgres.transaction(async trx => {
    const cart = await postgres.query.customerCarts.findFirst({
        where: and(
            eq(schema.customerCarts.accountId, accountId),
            eq(schema.customerCarts.wasOrdered, true)
        )
    });

    // Вложенные транзакции тоже поддерживаются
    await trx.transaction(nestedTrx => nestedTrx.query.accounts.findFirst({ where: eq(schema.accounts.id, accountId) }));

    // trx.rollback(); // Можно откатить транзакцию вручную
    // throw new Error('I will rollback this transaction'); // Или через прикидывание ошибки
}, {
    isolationLevel: "read committed", 
    accessMode: "read write",
    deferrable: true,
});
```

Репликация — в ручном режиме

На момент написания статьи Drizzle ORM не предоставляет встроенной поддержки работы с репликами read/write split. Тем не менее, ее можно реализовать вручную — например, через обертку над drizzle(...), создавая два подключения: одно для записи, другое для чтения.

Примерная структура:

const master = drizzle(process.env.WRITE_DB_URL);
const replica = drizzle(process.env.READ_DB_URL);

// Пишем в master
await master.insert(schema.logs).values({ message: 'hello' });

// Читаем из реплики
const logs = await replica.select().from(schema.logs);

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

Работа с расширениями для Drizzle ORM

Drizzle ORM поддерживает экосистему расширений, которые позволяют упростить интеграцию с другими библиотеками и улучшить developer experience при разработке. Ниже — два наиболее полезных.

drizzle-zod

Пакет drizzle-zod позволяет автоматически генерировать Zod-схемы на основе описанных в Drizzle таблиц. Это удобно для валидации входных данных, например, в API-обработчиках.

import { createInsertSchema } from 'drizzle-zod';
import { users } from './schema';

const insertUserSchema = createInsertSchema(users);

insertUserSchema— полноценная Zod-схема, которую можно использовать в API для проверки данных.

drizzle-orm/trpc

Для работы с tRPC можно использовать генерацию типов и схем напрямую из Drizzle — это позволяет избежать дублирования контрактов между бэкендом и фронтом.

import { inferRouterInputs, inferRouterOutputs } from '@trpc/server';

type AppRouter = typeof appRouter;

type Inputs = inferRouterInputs<AppRouter>;
type Outputs = inferRouterOutputs<AppRouter>;

Статически типизированные маршруты + строгие схемы данных на уровне API.

Интеграция с фреймворками

Drizzle можно легко использовать в любом современном TypeScript-приложении. Особенность — он не привязан к фреймворку, но спокойно работает с:

  • Next.js (App Router) — подключение через серверные обработчики;

  • Remix — через лоадеры и экшены;

  • NestJS — можно обернуть в провайдер или сервис, как любой другой клиент базы.

Гибкость Drizzle ORM позволяет внедрять инструмент поэтапно — без жестких требований к архитектуре приложения, но с полной типовой поддержкой на всех уровнях.

Ограничения или что стоит учитывать в работе с Drizzle ORM

Drizzle ORM решает многие проблемы классических ORM: он дает типы, остается ближе к SQL и не навязывает архитектуру. Но у него, как и у любого инструмента, есть свои ограничения. Они особенно заметны тем, кто приходит из мира Prisma или TypeORM.

Только SQL-базы

Drizzle работает только с реляционными СУБД: PostgreSQL, MySQL и SQLite. Если вы ищете поддержку MongoDB, Redis или другой NoSQL — этот инструмент не подойдет.

Больше контроля — меньше автоматизации

Drizzle не генерирует схемы «по моделям», не скрывает SQL и не предлагает магии. Это плюс, если вы хотите контролировать все вручную. Но если вы привыкли к удобству Prisma, например, когда можно не думать о связях и все описывается декларативно, то придется переключиться в другой режим работы.

Миграции — только через CLI

Применение миграций реализовано через drizzle-kit. В рантайме, при старте приложения, автоматическое применение миграций не предусмотрено. Это означает, что в CI/CD-процессе нужно явно учитывать миграции как отдельный шаг.

Нет встроенной репликации

Drizzle не управляет подключениями к репликам и не делает read/write split. Все переключение между инстансами баз, например, мастер и реплика — на стороне приложения.

Быстро развивающийся API

Проект активно развивается, и отдельные части экосистемы, например, drizzle-zod, drizzle-studio, могут меняться. Документация не всегда успевает за обновлениями — иногда придется смотреть в исходники.

Меньше готовых решений

Drizzle пока не может похвастаться большим количеством гайдов, Stack Overflow-ответов и туториалов. Для некоторых задач придется изобретать свое — зато часто это «чистый» TypeScript без лишней обвязки.


Drizzle ORM — хороший выбор, если вам нужен контроль, типизация и чистый SQL без лишнего слоя между вами и базой. Она не делает все за вас, но это понятный и стабильный инструмент, на который можно положиться в реальной работе.

Напишите в комментариях, если уже пробовали Drizzle. Будет интересно узнать, как вы его используете и с какими задачами он справился или, наоборот, не справился.

Комментарии (1)


  1. bikishov
    25.08.2025 14:41

    У drizzle плохо сделаны миграции, не возможно указать например timestamp или что-то кастомное, из-за чего при работе в команде возникают проблемы конфликты и одинаковые версии, но с разным sql кодом. И Kysely.js будет быстрее.