Настройка ORM Drizzle для веб-приложения
Drizzle — TypeScript ORM, где схема базы данных описывается на TypeScript, а не в отдельном DSL. Нет кодогенерации — типы выводятся напрямую из кода схемы. Это даёт более прозрачный стек: нет промежуточного слоя между кодом и базой, SQL всегда предсказуем.
Установка
# PostgreSQL
npm install drizzle-orm postgres
npm install -D drizzle-kit @types/pg
# MySQL
npm install drizzle-orm mysql2
npm install -D drizzle-kit
# SQLite / Turso (libSQL)
npm install drizzle-orm @libsql/client
Схема
// db/schema.ts
import {
pgTable, pgEnum, text, varchar, integer, decimal,
boolean, timestamp, uuid, index, uniqueIndex, primaryKey
} from 'drizzle-orm/pg-core'
import { relations } from 'drizzle-orm'
export const roleEnum = pgEnum('role', ['user', 'moderator', 'admin'])
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
email: varchar('email', { length: 255 }).notNull().unique(),
name: varchar('name', { length: 255 }).notNull(),
role: roleEnum('role').notNull().default('user'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
}, (table) => ({
emailIdx: uniqueIndex('users_email_idx').on(table.email),
}))
export const posts = pgTable('posts', {
id: uuid('id').primaryKey().defaultRandom(),
title: text('title').notNull(),
content: text('content'),
published: boolean('published').notNull().default(false),
authorId: uuid('author_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
viewCount: integer('view_count').notNull().default(0),
publishedAt: timestamp('published_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
}, (table) => ({
authorIdx: index('posts_author_idx').on(table.authorId),
publishedIdx: index('posts_published_idx').on(table.published, table.createdAt),
}))
export const tags = pgTable('tags', {
id: uuid('id').primaryKey().defaultRandom(),
name: varchar('name', { length: 100 }).notNull().unique(),
slug: varchar('slug', { length: 100 }).notNull().unique(),
})
export const postsToTags = pgTable('posts_to_tags', {
postId: uuid('post_id').notNull().references(() => posts.id, { onDelete: 'cascade' }),
tagId: uuid('tag_id').notNull().references(() => tags.id, { onDelete: 'cascade' }),
}, (table) => ({
pk: primaryKey({ columns: [table.postId, table.tagId] })
}))
// Relations — только для query builder
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}))
export const postsRelations = relations(posts, ({ one, many }) => ({
author: one(users, { fields: [posts.authorId], references: [users.id] }),
tags: many(postsToTags),
}))
Конфигурация миграций
// drizzle.config.ts
import type { Config } from 'drizzle-kit'
export default {
schema: './db/schema.ts',
out: './drizzle',
driver: 'pg',
dbCredentials: {
connectionString: process.env.DATABASE_URL!
},
verbose: true,
strict: true,
} satisfies Config
# Генерация SQL миграции
npx drizzle-kit generate:pg
# Применить миграции
npx drizzle-kit push:pg # для dev
npx drizzle-kit migrate # через миграционный файл
# Проверить текущий статус
npx drizzle-kit check:pg
Инициализация подключения
// db/index.ts
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import * as schema from './schema'
const connectionString = process.env.DATABASE_URL!
// Для миграций — один коннект
const migrationClient = postgres(connectionString, { max: 1 })
// Для приложения — пул
const queryClient = postgres(connectionString, {
max: 20,
idle_timeout: 30,
connect_timeout: 10,
})
export const db = drizzle(queryClient, { schema, logger: process.env.NODE_ENV === 'development' })
Запросы
import { db } from '@/db'
import { users, posts, tags, postsToTags } from '@/db/schema'
import { eq, and, desc, ilike, sql, count, inArray } from 'drizzle-orm'
// Простой select
const user = await db.query.users.findFirst({
where: eq(users.email, '[email protected]'),
with: {
posts: {
where: eq(posts.published, true),
limit: 5,
orderBy: [desc(posts.createdAt)]
}
}
})
// Insert с возвратом
const [newPost] = await db
.insert(posts)
.values({ title, content, authorId: userId })
.returning()
// Update
await db
.update(posts)
.set({ published: true, publishedAt: new Date() })
.where(and(eq(posts.id, postId), eq(posts.authorId, userId)))
// Поиск с пагинацией
async function searchPosts(query: string, page: number, limit = 20) {
const offset = (page - 1) * limit
const [items, [{ total }]] = await Promise.all([
db.select({
id: posts.id,
title: posts.title,
createdAt: posts.createdAt,
authorName: users.name
})
.from(posts)
.innerJoin(users, eq(posts.authorId, users.id))
.where(and(
eq(posts.published, true),
ilike(posts.title, `%${query}%`)
))
.orderBy(desc(posts.createdAt))
.limit(limit)
.offset(offset),
db.select({ total: count() })
.from(posts)
.where(and(eq(posts.published, true), ilike(posts.title, `%${query}%`)))
])
return { items, total, pages: Math.ceil(total / limit) }
}
// Сырой SQL для сложных случаев
const stats = await db.execute(sql`
SELECT
date_trunc('day', created_at) AS day,
count(*) AS post_count
FROM posts
WHERE published = true
AND created_at >= now() - interval '30 days'
GROUP BY 1
ORDER BY 1
`)
Транзакции
const result = await db.transaction(async (tx) => {
const [user] = await tx
.insert(users)
.values({ email, name })
.returning()
await tx.insert(profiles).values({ userId: user.id })
return user
})
Сравнение с Prisma
Drizzle ближе к SQL: JOIN-запросы более явные, нет магии include. Это плюс, если важна предсказуемость генерируемого SQL и максимальная производительность. Prisma удобнее для команд, которым важна скорость написания CRUD без глубокого знания SQL. Drizzle хорошо работает с Edge Runtime (Cloudflare Workers, Vercel Edge) — Prisma там ограничена.
Сроки
Настройка Drizzle с нуля (схема, миграции, типизированный клиент): 1 день. Полная интеграция с репозиторием и тестами: 1–2 дня. Портирование с Prisma на Drizzle в существующем проекте: 2–4 дня.







