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
Что делает: Выполняет несколько операций в рамках одной транзакции. В случае ошибки все изменения откатываются.
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