Налаштування 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) }
}
// Raw 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
})
Drizzle vs 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 дні.







