Drizzle ORM для TypeScript: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Drizzle ORM для TypeScript: Полное руководство

1. Что такое Drizzle ORM и зачем он нужен?

Drizzle ORM — это современная, лёгкая и type-safe ORM (Object-Relational Mapping) библиотека для TypeScript и JavaScript. В отличие от тяжеловесных ORM вроде TypeORM или Sequelize, Drizzle спроектирована с фокусом на производительность, предсказуемость и полную интеграцию с системой типов TypeScript. Она не пытается скрыть SQL, а наоборот, предоставляет удобный и типобезопасный способ писать SQL-запросы, сохраняя при этом полный контроль над генерируемым кодом.

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

Drizzle идеально подходит для проектов, где важна производительность и контроль над SQL, но при этом хочется иметь удобства ORM: автодополнение в IDE, проверку типов и декларативное описание схемы. Она часто используется в связке с Next.js, NestJS, Express и другими фреймворками. Благодаря своей лёгкости (около 7KB gzipped) и отсутствию магии, Drizzle является отличным выбором как для небольших проектов, так и для крупных enterprise-приложений.

2. Установка

Установка Drizzle ORM и его CLI-инструмента (Drizzle Kit) производится через npm или yarn. Выбор драйвера зависит от используемой базы данных.

# Для PostgreSQL
npm install drizzle-orm pg
npm install -D drizzle-kit @types/pg

# Для MySQL
npm install drizzle-orm mysql2
npm install -D drizzle-kit @types/node

# Для SQLite
npm install drizzle-orm better-sqlite3
npm install -D drizzle-kit @types/better-sqlite3

После установки необходимо создать файл конфигурации drizzle.config.ts для Drizzle Kit:

import type { Config } from "drizzle-kit";

export default {
  schema: "./src/db/schema.ts",
  out: "./drizzle",
  dialect: "postgresql", // или "mysql", "sqlite"
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
} satisfies Config;

3. Быстрый старт

Минимальный рабочий пример для PostgreSQL. Сначала определяем схему, затем создаём подключение и выполняем запрос.

// src/db/schema.ts
import { pgTable, serial, text, integer } from "drizzle-orm/pg-core";

export const users = pgTable("users", {
  id: serial("id").primaryKey(),
  name: text("name").notNull(),
  age: integer("age"),
});

// src/index.ts
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
import { users } from "./db/schema";
import { eq } from "drizzle-orm";

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
});

const db = drizzle(pool);

async function main() {
  // Вставка данных
  const newUser = await db.insert(users).values({
    name: "Alice",
    age: 30,
  }).returning();
  console.log("Created user:", newUser);

  // Выборка данных
  const allUsers = await db.select().from(users);
  console.log("All users:", allUsers);

  // Выборка с условием
  const user = await db.select()
    .from(users)
    .where(eq(users.name, "Alice"));
  console.log("Found user:", user);
}

main().catch(console.error);

4. Основные методы и функции

Ниже приведены ключевые методы Drizzle ORM с сигнатурами и примерами.

4.1 drizzle(client)

Сигнатура: drizzle(client: Client, config?: DrizzleConfig): DrizzleDB

Что делает: Создаёт экземпляр базы данных Drizzle на основе переданного клиента (Pool, Connection и т.д.). Это главная точка входа для всех операций.

import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";

const pool = new Pool({ connectionString: "..." });
const db = drizzle(pool);

4.2 db.select()

Сигнатура: db.select(fields?: SelectedFields): QueryBuilder

Что делает: Инициирует SELECT запрос. Позволяет выбрать все поля или конкретные колонки из таблицы. Возвращает объект QueryBuilder для дальнейшего построения запроса.

import { users } from "./schema";

// Выбрать все поля
const allUsers = await db.select().from(users);

// Выбрать только имя и возраст
const names = await db.select({
  name: users.name,
  age: users.age,
}).from(users);

4.3 db.insert(table)

Сигнатура: db.insert(table: Table): InsertQueryBuilder

Что делает: Инициирует INSERT запрос в указанную таблицу. Позволяет вставлять одну или несколько записей.

import { users } from "./schema";

// Вставка одной записи
await db.insert(users).values({ name: "Bob", age: 25 });

// Вставка нескольких записей
await db.insert(users).values([
  { name: "Charlie", age: 35 },
  { name: "Diana", age: 28 },
]);

// Вставка с возвратом данных (PostgreSQL)
const inserted = await db.insert(users)
  .values({ name: "Eve", age: 22 })
  .returning();

4.4 db.update(table)

Сигнатура: db.update(table: Table): UpdateQueryBuilder

Что делает: Инициирует UPDATE запрос для указанной таблицы. Требует обязательного вызова .set() и .where().

import { users } from "./schema";
import { eq } from "drizzle-orm";

await db.update(users)
  .set({ age: 31 })
  .where(eq(users.name, "Alice"));

4.5 db.delete(table)

Сигнатура: db.delete(table: Table): DeleteQueryBuilder

Что делает: Инициирует DELETE запрос. Рекомендуется всегда использовать .where(), чтобы избежать удаления всех записей.

import { users } from "./schema";
import { eq } from "drizzle-orm";

await db.delete(users)
  .where(eq(users.name, "Bob"));

4.6 eq, ne, gt, lt, gte, lte (условные операторы)

Сигнатура: eq(column: Column, value: any): SQLCondition

Что делает: Создают условия для WHERE, HAVING и ON частей запроса. Обеспечивают type-safe сравнение.

import { users } from "./schema";
import { eq, gt, and, or } from "drizzle-orm";

// Простое условие
const user = await db.select()
  .from(users)
  .where(eq(users.name, "Alice"));

// Сложное условие: возраст больше 25 И имя Alice
const result = await db.select()
  .from(users)
  .where(and(gt(users.age, 25), eq(users.name, "Alice")));

// ИЛИ: возраст меньше 20 или больше 60
const result2 = await db.select()
  .from(users)
  .where(or(lt(users.age, 20), gt(users.age, 60)));

4.7 db.select().from(table).leftJoin() / innerJoin() / rightJoin()

Сигнатура: .leftJoin(table: Table, on: SQLCondition): QueryBuilder

Что делает: Выполняет JOIN между таблицами. Поддерживаются LEFT, INNER, RIGHT и FULL JOIN.

import { users, posts } from "./schema";
import { eq } from "drizzle-orm";

const result = await db.select()
  .from(users)
  .leftJoin(posts, eq(users.id, posts.userId));

4.8 db.transaction(callback)

Сигнатура: db.transaction(callback: (tx: TransactionDB) => Promise): Promise

Что делает: Выполняет несколько операций в рамках одной транзакции. В случае ошибки все изменения откатываются.

import { users, posts } from "./schema";

await db.transaction(async (tx) => {
  const newUser = await tx.insert(users)
    .values({ name: "Frank", age: 40 })
    .returning();

  await tx.insert(posts)
    .values({ title: "First Post", userId: newUser[0].id });
});

4.9 sql\`...\` (сырые SQL-запросы)

Сигнатура: sql\`query\` (теговый шаблон)

Что делает: Позволяет выполнять произвольные SQL-запросы, когда возможностей ORM недостаточно. Автоматически экранирует параметры.

import { sql } from "drizzle-orm";

const result = await db.execute(
  sql`SELECT * FROM users WHERE age > ${25}`
);

4.10 db.query.table.findMany() (Relational Queries)

Сигнатура: db.query.users.findMany(args?: FindManyArgs): Promise

Что делает: Использует реляционный API для выборки данных с автоматической загрузкой связанных сущностей. Требует предварительного описания связей в схеме.

import { users, posts, relations } from "./schema";

// В схеме должны быть определены relations
const result = await db.query.users.findMany({
  with: {
    posts: true, // автоматически загрузит посты пользователя
  },
  where: (users, { eq }) => eq(users.name, "Alice"),
});

5. Реальный практический пример

Создадим простой REST API для блога на Express с использованием Drizzle ORM и PostgreSQL.

// src/db/schema.ts
import { pgTable, serial, text, integer, timestamp, boolean } from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm";

export const users = pgTable("users", {
  id: serial("id").primaryKey(),
  email: text("email").notNull().unique(),
  name: text("name").notNull(),
  createdAt: timestamp("created_at").defaultNow(),
});

export const posts = pgTable("posts", {
  id: serial("id").primaryKey(),
  title: text("title").notNull(),
  content: text("content").notNull(),
  published: boolean("published").default(false),
  authorId: integer("author_id").notNull().references(() => users.id),
  createdAt: timestamp("created_at").defaultNow(),
});

export const usersRelations = relations(users, ({ many }) => ({
  posts: many(posts),
}));

export const postsRelations = relations(posts, ({ one }) => ({
  author: one(users, {
    fields: [posts.authorId],
    references: [users.id],
  }),
}));

// src/index.ts
import express from "express";
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
import { users, posts } from "./db/schema";
import { eq, and, desc } from "drizzle-orm";

const app = express();
app.use(express.json());

const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const db = drizzle(pool);

// GET /users — список пользователей с количеством постов
app.get("/users", async (req, res) => {
  const allUsers = await db.query.users.findMany({
    with: {
      posts: {
        columns: {
          id: true,
        },
      },
    },
  });

  const result = allUsers.map(user => ({
    ...user,
    postCount: user.posts.length,
  }));

  res.json(result);
});

// POST /users — создание пользователя
app.post("/users", async (req, res) => {
  const { email, name } = req.body;
  
  try {
    const newUser = await db.insert(users)
      .values({ email, name })
      .returning();
    
    res.status(201).json(newUser[0]);
  } catch (error) {
    res.status(400).json({ error: "Email already exists" });
  }
});

// GET /posts — список опубликованных постов с авторами
app.get("/posts", async (req, res) => {
  const publishedPosts = await db.query.posts.findMany({
    where: (posts, { eq }) => eq(posts.published, true),
    with: {
      author: true,
    },
    orderBy: (posts, { desc }) => [desc(posts.createdAt)],
  });

  res.json(publishedPosts);
});

// POST /posts — создание поста
app.post("/posts", async (req, res) => {
  const { title, content, authorId } = req.body;

  const newPost = await db.insert(posts)
    .values({ title, content, authorId })
    .returning();

  res.status(201).json(newPost[0]);
});

// PUT /posts/:id/publish — публикация поста
app.put("/posts/:id/publish", async (req, res) => {
  const postId = parseInt(req.params.id);

  const updatedPost = await db.update(posts)
    .set                    

Рекомендаций