Пераход з Prisma на Drizzle: аналіз за шасць месцаў
Шысь месяцаў пасля перанесення стака PostgreSQL TypeScript з Prisma на Drizzle ORM: рэальныя тэсты, што паляпілася адразу, і недагоды, якія завадзілі.
Крыніцы: I replaced Prisma with Drizzle. Six months later, here’s what I gained and what I missed.
Прыблызна за восемь месяцэў непакойныя сумневы ў Prisma нарэшце ператворыліся на ўсё, што было вартае дзейства. Не тое каб ўсё было абоўсюды зламанае — трыбоцтва проста паступова нараблялася з часам, пакуль стала немагчымай не зважаць на яе. Система складалася з кальколькох маленьких прыемоў, усіх запускалых на PostgreSQL, напісаных на TypeScript, і якія знаходзіліся за падтрымкай кальколькох API-сэрвісаў. Нічога не звычайнага чы роўна прыголомшальнага. Prisma выканала тое, што яе і мяла робіць. Аднак продовжліва з’являліся кальколькі перадумоўкаючых проблем.
Створаны файл @prisma/client быў вельмі вялікі. Калі ён выкананы на сервернасабліковых функціях, процес запуску значна сповольняўся, таму што спачатку трэба было завантажыць кліент. Крок prisma generate у CI-пайплайне пачаў вярцца як платная служба, якая стоіла за кожны процес злучэння коду. А калі для чаго-небудзь болей складнага, чым могла выразіць ORM, была патрэбна необработаная запытка, тип рэзультата вяртаўся як unknown[], чым не была гарантавана ніякая безпека типу, і дзеўялося неабходнасць ручных пераканстанняў.
У канцы канцоў была прынята рашынка перайсці на Drizzle ORM, а міграцыя была завершана прыблізна шэсць месцаў таму. Чытача будзе прадставлены адкрытыя відгуки пра тое, як усё гэта праходзіла.
Чаму я ўжо тады расследаваў Drizzle
Проблема з размахам кантэйнера была першым справжнім стимулам для серйознае адносленняся да Drizzle. У момент міграцыі сам кліент Prisma дадаў практычна 14 МБ да пакета развёртвання Lambda, пры чым сам бінарны файл двойчака Rust становіў калькуючыяся з таго колька мегабайтаў. Канцэнтры, якія не адвідаваліся часта, мелі час запуску 2–3 секунды — такія показнікі адразу ж прывертаюць увагу на панелі статыстык затрымкаў.
У працоўны час Drizzle, у парадку порэвання, займае адносна 7,4 КБ пасля мініфікацыі і стыпанування, пры чым не мае жадных залежнасцяў пад час выканання. Не існуе скомпіляванага бінарнага файлу, няма окранучага процеса двойчака, няма крока серыялізацыі межу вашым кодам на TypeScript і з’ўязкам з базай дадзенаў.
Другым джэрелам супершчоткі быў крок генеравання коду. Будзь-яя змена схемы значыла неабходнасць запуску prisma generate, прычаму TypeScript мог ўпісаць новую структуру. На практыцы гэта прыводзіла да двух паслядків: або вы запускалі гэты крок ручна і часам забывалі зафіксаваць генераваны код, або інтегравалі яго ў кожны запуск CI, чыяму кожная перабудова займала на 15–20 секунд больш. Жадна з гэтых апцыяў не была задовальнай.
З Drizzle сама схема — гэта проста TypeScript. Зменіце файл схемы, і типы адразу ж апэндруюцца; няма кроку генеравання, і компілятор адразу ж фіксуе змяну.
Трэцім мотывам было тое, як адрабатавалася необработаная SQL. Prisma спрацоўвае з яю выключна як альтэратыва: вызов $queryRaw вяртае unknown[], і ў такім случае для безпечнага викорыстання патрэбна або ручная кастаўка, або схема Zod. У свой час тэг шаблона sql у Drizzle дазволяе безпасова заявіць форму даных — напрыклад, sql<{ id: number; total: number }> — і вы отрымаеце самэ гэты тип, без кастаўкі і без дадатковага шару верыфікацыі.
Як выглядала міграцыя
Для ілюстрацыі розглянем тры табелі: users, products і orders. У Prisma яны былі заданы ў аднам файле schema.prisma:
model User {
id String @id @default(uuid())
email String @unique
name String
orders Order[]
createdAt DateTime @default(now())
}
model Product {
id String @id @default(uuid())
name String
priceInCents Int
orders OrderItem[]
}
model Order {
id String @id @default(uuid())
userId String
user User @relation(fields: [userId], references: [id])
items OrderItem[]
createdAt DateTime @default(now())
}
model OrderItem {
id String @id @default(uuid())
orderId String
order Order @relation(fields: [orderId], references: [id])
productId String
product Product @relation(fields: [productId], references: [id])
quantity Int
}
У Drizzle тая ж самая схема становіцца файлам на TypeScript:
import { pgTable, uuid, text, integer, timestamp } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';\
export const users = pgTable('users', {
id: uuid('id').defaultRandom().primaryKey(),
email: text('email').notNull().unique(),
name: text('name').notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
export const products = pgTable('products', {
id: uuid('id').defaultRandom().primaryKey(),
name: text('name').notNull(),
priceInCents: integer('price_in_cents').notNull(),
});
export const orders = pgTable('orders', {
id: uuid('id').defaultRandom().primaryKey(),
userId: uuid('user_id').notNull().references(() => users.id),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
export const orderItems = pgTable('order_items', {
id: uuid('id').defaultRandom().primaryKey(),
orderId: uuid('order_id').notNull().references(() => orders.id),
productId: uuid('product_id').notNull().references(() => products.id),
quantity: integer('quantity').notNull(),
});
export const usersRelations = relations(users, ({ many }) => ({
orders: many(orders),
}));
export const ordersRelations = relations(orders, ({ one, many }) => ({
user: one(users, { fields: [orders.userId], references: [users.id] }),
items: many(orderItems),
}));
export const orderItemsRelations = relations(orderItems, ({ one }) => ({
order: one(orders, { fields: [orderItems.orderId], references: [orders.id] }),
product: one(products, { fields: [orderItems.productId], references: [products.id] }),
}));
Так, ўсё працюе трохі дольшэ. Але гэта справжні TypeScript з самага пачатку і даўна. Ваш рэдагувальнік разумее кожны поле схемы, функцыя автодаполнення працюе безпаследкова з яйго, а вы можетэ імпортаваць часткі схемы прымусова ў логіку вашага прыкладнага програму.
Файл drizzle.config.ts аб’еднвае ўсё разам:
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
dialect: 'postgresql',
schema: './src/schema.ts',
out: './drizzle/migrations',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
strict: true,
});
Зважайце: завжды задаюце strict: true. Якщо гэта працявало без цігу, Drizzle можа спрыйняць перайменаванне столбца як „выдаліць стары столбец, створыць новы“ — такія змены таямніча знишчаюць даныя, пакуль вы не пераглянете створаны SQL-код, каб зрозумець, што сталося. Увімкненне strict: true змушвае Drizzle на момент зупініцца і запытаць пра падтверджэнне кожны раз, калі змена є неодназначной.
Коли схема вже створана, стварэнне міграцыі адбываецца за адной командай:
bunx drizzle-kit generate
Гэта стварае файл SQL у падзеці ./drizzle/migrations. Вы яго адгледзіце, пераканаецеся, што ён выконвае тое, чаго яшчэце, а пасля застосавіце яго:
bunx drizzle-kit migrate
Гэты файл SQL пракладаецца ў систему кантролю версій прыкладзеным да схемы. Весь історыя міграцый прыводзіцца у вобразе звычайнага, чытальнага SQL, а не у формате логу, які ўзрозумелы толькі за дапамогою сопрацоўнічых засобаў Prisma.
Што пакращылася адразу
Меньшыя пакеты, быстрэйшы запуск функцый. Гэта стала зрозумелаю адразу і было лёгка яе вымерыць. Размеры пакетаў Lambda зменіліся на больш чым 90 процэнтав. Функцыі, якія раней заставаліся пад два секунды на запуск, тепер запускаюцца за менш чым 400 мс. Фонавыя задачы не пакрыліся значным прыбуткам ад гэтага, але нашы функцыі API значна праспявалі.
Тэкст SQL у вогненным формате. Гэта стала корыстнаю працоўнай засобам яшчэ на самым другі дзень міграцыі. У нас была запитаўка для стварэння звітавання, якая спаўнала аб’еднанне чатырох тэблей з агрегацыямі, якія прыстройнік запитаў Prisma проста не могаў выразыць чыстым спосабам. Альтернатыўны варіант Prisma для такога роду запитаў выглядаў так:
// Prisma - returns unknown[], no type safety
const revenue = await prisma.$queryRaw`
SELECT p.id, p.name, SUM(oi.quantity * p.price_in_cents) as total_revenue
FROM products p
JOIN order_items oi ON p.id = oi.product_id
GROUP BY p.id, p.name
ORDER BY total_revenue DESC
`;
З Drizzle адпаведны запит вяртаецца з рэальным типам:
import { sql } from 'drizzle-orm';
interface ProductRevenue {
id: string;
name: string;
totalRevenue: number;
}
const revenue = await db.execute(sql<ProductRevenue>`
SELECT p.id, p.name, SUM(oi.quantity * p.price_in_cents) AS total_revenue
FROM products p
JOIN order_items oi ON p.id = oi.product_id
GROUP BY p.id, p.name
ORDER BY total_revenue DESC
`);
// revenue.rows typed as ProductRevenue[] at compile-time; validate at runtime if needed
Вы все равно пісаеце інтэфейс вручную, а не дазволяеце системе яго автаматычна выважваць, але гэта незначныя затраты паўступаючы да викорыстанняя прымусовых перакладоў unknown у всій базе коду.
Drizzle Studio. Запуск bunx drizzle-kit studio стварае браузер локальнай базы дадзеных. Ён менш працэйны, чым Prisma Studio, ал ён швыдкі, функцыйональны і не трэбуе акаунта Prisma чыраго з’яўлення ў хмаре. Для повсякдзенной роботы на месцы такая незалежнасць мае вялікую цэну.
Што патрэбавалася налаштовання
Для стварэння зв’язкаў трэба болей чыткія настройкі. У Prisma адзержванне замовлення разам з яго корыстнікам і элементамі было проста — достатна было выкарыстоўваць include:
// Prisma - nested include
const order = await prisma.order.findUnique({
where: { id: orderId },
include: {
user: true,
items: {
include: { product: true }
}
}
});
API для запытоў зв’язкаў у Drizzle даходзіць да падобнага рэзультата:
import { db } from './db';
import { orders, users, orderItems, products } from './schema';
const order = await db.query.orders.findFirst({
where: (orders, { eq }) => eq(orders.id, orderId),
with: {
user: true,
items: {
with: { product: true }
}
}
});
Фінальная форма двух запросаў ў большай меры аднаковая. Але проблема тая, што синтаксіс with працюе толькі пасля таго, як вы зададзілі вызывы relations() у сваёй схеме, тыя самыя, якія былі описаны раней. Калі мы першы раз перайшлі на ўжыванне гэтага падходу, мы ўсё ўжо не завершылі настройку, і нам давелася витраціць частацу дня, намагаючыся з’ясаваць, чаму with не вяртае нічога. Калі асоціяцыяў правильна заданы, усё працюе як і планавалася — але гэты початковы крок настройкі Prisma выкаанае за вас апошнюю меру на основе самай схемы, без дадатковых налаштаванняў.
Настройка начальных дадзенняў тепер ёсць ручной процес. Prisma мае вбудованую прыему для настройкі начальных дадзенняў: пакладзіце файл seed.ts на належнае месца, запусціце prisma db seed, і гэта ўсё. У Drizzle няма адпаведнага вбудованага каманды. У замене вы пішаце звычную скрыпту на TypeScript і самі яе запускаце:
import { db } from './db';
import { users, products } from './schema';
async function seed() {
await db.insert(users).values([
{ email: 'ana.silva@acme.com', name: 'Ana Silva' },
{ email: 'carlos.reyes@acme.com', name: 'Carlos Reyes' },
]);
await db.insert(products).values([
{ name: 'Starter Plan', priceInCents: 2900 },
{ name: 'Pro Plan', priceInCents: 7900 },
{ name: 'Enterprise Plan', priceInCents: 19900 },
]);
console.log('Seed complete');
process.exit(0);
}
seed().catch(console.error);
І вы насталявайце гэта ў package.json:
{
"scripts": {
"db:seed": "bun run src/seed.ts"
}
}
Нічага з гэтага ня ёсць складным, і, можна сказаць, гэта прозрачней, чым неявныя правілы Prisma. Але будзь-якія існуючыя інструменты чы ўзроўныя крокі, якія безпасебна вызывалі prisma db seed, будуць павінны быць перапісаныя, каб вызывалі ваш сопны скрыпт у замену.
Працоўнае паўстанавленне двух студыёў. Drizzle Studio выканае задачу. Prisma Studio проста выглядае краща і праслужвае больш: убудованыя элементы керавання фільтрамі, пагінацыя і болей вывернуты рэдагувальнік табелей. У працоўнае паўстанавленне, Drizzle Studio здаецца простым. Чэрез шасць месцаў выкарыстоўвання Drizzle, мы ў канцы канца паслугаваліся обомі інструментамі менш, чым спакушаліся, адтуды што большасць рэальнага дакладжання дадзеных адбываецца через спецыяльны кліент базы дадзеных, незалежна ад таго, які ORM вы выкарыстоўвалі.
Едзіны случай, калі Prisma яшчэ выграў
Змешаныя запісі. Калі вам трэба адгрупаваць замовленне разам з яго элементамі ў одны крок, Prisma справляецца з гэтым чыста:
// Prisma - nested create in one call
const order = await prisma.order.create({
data: {
userId: user.id,
items: {
create: [
{ productId: starterPlanId, quantity: 1 },
{ productId: proPlanId, quantity: 2 },
]
}
},
include: { items: true }
});
У Drizzle няма адпаведнага рашэння для змешаных стварэнняў. У замест на гэта вам павіннаеся самім напісаць логіку ў межах явнай транзакцыі:
import { db } from './db';
import { orders, orderItems } from './schema';
const newOrder = await db.transaction(async (tx) => {
const [order] = await tx
.insert(orders)
.values({ userId: user.id })
.returning();
await tx.insert(orderItems).values([
{ orderId: order.id, productId: starterPlanId, quantity: 1 },
{ orderId: order.id, productId: proPlanId, quantity: 2 },
]);
return order;
});
Гэта значна больш коду, які трэба пісаць і падтрымляць. Якщо ваша аплікацыя сильна завісіць ад такога патэрану, додатковы код шыркаеся быстра. Версія Prisma залишаецца корачэй і болей прымоўна выражае намер. Якщо змешаныя мутацыі часта з’являюцца ў вашай модэлі домэна, выбор Prisma — абсалютна разумны выбар.
Рашэнне пра переход
Разглядзіце можлівасць перайсці на Drizzle, якщо паўтрапляе хоць адна з наступных ситуацыяў: вы викорыстоўваете серверлесс- або edge-платформы, дзе размер пакета і час запуску адчувальна вплываюць на выкладкі чыстаўца або якасць ўжывання; вы часта пішчыце складныя аналітычныя SQL-запиты і хочаце магчымасці прямога доступу да іх; ваша команда дастаткова вольна володзіць SQL, таму шар абстракцыі не даўае значныя перадчынства; або ж вы хочаце, каб міграцыі былі выражаныя як звычныя SQL-файлы, якія будзь-яны інжынер, незалежна ад знанняў ORM, можа прачытаць і пераглядзець.
Заставайцеся з Prisma, якщо: вы працуеце над пратотыпам чыстаўца або внутранім інструментам, дзе шырокая швальнасць мае большое значэнне, чым заўзятая еканомія мілісекунд у часе выконання; ваш чыстаўець ведае багато запісаў і чытанняў з глыбокаямі вкладзенымі асоціяцыямі; частка вашай команды менш вольна володзіць чыстым SQL і отрымае перадчынства ад декларатывнага стылю моделювання Prisma; або ж вы вялікі час яшчэ перайшлі на Prisma 7, дзе прыгожыя проблемы з размерам пакета ўжо не такія серьёзныя.
Часы таксамо маюць значэнне. Prisma 7, выданы ў канцы 2025 года, цэлком пазбавіўся двайчы канвэра Rust. Яго ныяны размер складае або-та 1,6 МБ — гэта яшчэ болей чым у 200 разоў больш, чым размер пакета Drizzle, але далёка ад 14 МБ бінарнага файла, які спачатку прымусваў людзей шукать альтернатывы. Якщо ўжо толькі размер пакета быў прычыной, чаму вы расследавалі можлівасць пераходу, варта пераканацца, чыя ж гэта прычына застаецца актуальной, вярнуўшыся да таго, насколькі Prisma скармалелася.
Для команды, якая стояла за гэтым пераходам, адказ быў яшчэ «так». Кращыя типізацыі для прыказаў raw-query і відсутнасць крока генеравання коду самыя по сабе прабачалі пераход, незалежна ад будь-якых заўтрэбнай эканоміі памяці. Паглядзевшы назад чераз шасць месцаў, рашэнне застаецца правым. Аднак гэта не ўніверсальна правильная адказ — кожны, хто стварае вражанне, што адны ORM об’ектыўна лепш для кожнага випадку выкарыстання, не говорыць вам правду.
Наступныя крокі, якщо вы хочаце самі расследаваць гэта
Якщо вы хочаце эксперыментаваць з Drizzle проты якога-небудзь вялікага базы дадзеных, яка ўжо у вас є, не бяручы на сябе абавязак перапісваць ўсё з нуля, інструменты для інтроспекціі Drizzle Kit дазваляюць drizzle-kit pull чытаць вашае існуючае схематызаванне і автаматычна ствараць адпаведныя ваказанні TypeScript. Це дае вам рабочую базу для працы замест таго, каб пісаць кожнае ваказанне табелі з нуля.
Афіцыйная дакументацыя Drizzle ORM практычна расказвае пра будову кэшэнтных запытаў, аператар шаблона sql і полны набор каманд для міграцый. Таксама варта прачытаць стороннюю сторанку порэванаў Prisma з Drizzle — у яй чыста прадставлены ўсі плюсы і мінусы з их точкі зору, што ўсё адкрытэй, чым большасць іншых порэванаў відпаведных продуктав.