· typescript / orm / database

Cách chuyển từ Prisma sang Drizzle ORM: Hướng dẫn từng bước

Hướng dẫn từng bước chuyển ứng dụng TypeScript từ Prisma sang Drizzle: đổi schema, viết lại query, xử lý transaction và tám cạm bẫy mà hầu hết hướng dẫn bỏ qua.

Bởi · Cập nhật 26 tháng 5, 2026

2.447 từ · 13 phút đọc

Chuyển từ Prisma sang Drizzle đáng làm nếu bạn đang nhắm đến Cloudflare Workers, D1, hoặc bất kỳ edge runtime nào có giới hạn bundle size nghiêm ngặt. Nếu bạn đang dùng Prisma 7 trên Lambda hoặc container thông thường, khoảng cách cold-start đã thu hẹp đến mức chi phí chuyển đổi hiếm khi bù đắp được lợi ích. Bài này hướng dẫn cả hai trường hợp — và tám điểm mà quá trình migration sẽ khiến bạn bất ngờ.

Đã kiểm thử với [email protected]@prisma/[email protected].

Bài này dành cho ai

Các developer đang dùng Prisma trong production và đang cân nhắc hoặc thực hiện migration sang Drizzle. Bạn cần quen với TypeScript và có khả năng đọc hiểu SQL cơ bản — API cốt lõi của Drizzle ánh xạ trực tiếp đến SQL và ẩn ít chi tiết hơn Prisma.

Nếu bạn chưa quyết định có nên chuyển hay không, đọc Prisma vs Drizzle trước. Bài đó so sánh trực tiếp. Bài này giả định bạn đã quyết định và cần triển khai.

Lý do mọi người chuyển sang Drizzle

Bundle size và cold start

Prisma trước đây đi kèm một binary Rust cho query engine (~14 MB). Prisma 7 (tháng 11/2025) thay thế nó bằng TypeScript/WASM, giảm bundle xuống còn ~1.6 MB. Bundle của Drizzle là ~57–67 KB và không có binary dependency nào.

Một SaaS thực tế ghi nhận cold start trên Vercel Edge giảm từ 820 ms xuống 210 ms sau khi chuyển sang Drizzle — 67 KB so với 3.2 MB của Prisma.

Điểm cần chú ý: Prisma 7 đã thu hẹp đáng kể khoảng cách trên Lambda và server thông thường. Lý do dùng Drizzle vì cold-start thuyết phục nhất ở edge, không phải môi trường serverless truyền thống.

Cloudflare D1 là điều kiện bắt buộc

Prisma không hỗ trợ Cloudflare D1. Drizzle có, với một adapter riêng. Nếu bạn xây dựng trên Cloudflare stack thì đây không phải là đánh đổi — mà là yêu cầu bắt buộc.

Không cần codegen

Prisma tạo client mỗi lần chạy prisma generate. Với schema 50 model, bước này cộng thêm thời gian đáng kể vào CI và đòi hỏi một build step trước khi các type khả dụng. Drizzle suy ra type trực tiếp từ khai báo schema — không cần bước generate, không có artifact trung gian.

Điều kiện cần và những gì cần chuẩn bị

Migration phù hợp với: developer solo hoặc team nhỏ quen SQL, ứng dụng trên PostgreSQL, MySQL, hoặc SQLite, team nhắm đến serverless hoặc edge runtime.

Nên chờ nếu: team có nhiều query include lồng nhau mà không quen SQL (Drizzle dùng join tường minh, cần nhiều code hơn cho cùng kết quả), ứng dụng dùng MongoDB hoặc MSSQL (Drizzle không hỗ trợ), hoặc team phụ thuộc nhiều vào Prisma Studio như GUI chính.

Ước tính công sức: một migration cộng đồng của API 10 tháng tuổi với 19 bảng mất khoảng 21 giờ cho một developer, sau đó giảm được ~25% số dòng code. Nguồn: GitHub Discussions #3146.

Chiến lược được khuyến nghị: đừng xóa Prisma ngay từ đầu. Thêm Drizzle song song, migrate từng bảng một, rồi mới xóa Prisma sau cùng. Cách này loại bỏ rủi ro của một lần chuyển đổi toàn bộ.

Hướng dẫn từng bước

Bước 0: tùy chọn — chạy Drizzle song song với Prisma

Tích hợp Drizzle-Prisma cho phép bạn dùng chung kết nối Prisma và migrate dần dần:

npm i drizzle-orm drizzle-prisma-generator

Thêm một generator vào schema.prisma:

generator drizzle {
  provider = "drizzle-prisma-generator"
  output   = "./drizzle"
}

Chạy prisma generate. Lệnh này tạo các file Drizzle schema từ Prisma schema của bạn. Query qua property $drizzle:

const user = await prisma.$drizzle.select().from(users).where(eq(users.id, id));

Hạn chế đã biết: relational API của Drizzle (db.query.X.findFirst({ with: ... })) không dùng được qua extension này, SQLite .values() không được hỗ trợ, và prepared statement chỉ hỗ trợ một phần. Mode này dùng tốt cho migration dần dần; hãy lên kế hoạch chuyển hoàn toàn sau khi đã convert xong tất cả các bảng.

Bước 1: cài Drizzle ORM và Drizzle Kit

# PostgreSQL
npm i drizzle-orm pg
npm i -D drizzle-kit @types/pg

# MySQL
npm i drizzle-orm mysql2
npm i -D drizzle-kit

# SQLite
npm i drizzle-orm better-sqlite3
npm i -D drizzle-kit @types/better-sqlite3

Để thiết lập đầy đủ kết nối Postgres sau khi migration, xem Hướng dẫn cài đặt Drizzle ORM với Postgres.

Bước 2: cấu hình drizzle.config.ts

import { defineConfig } from 'drizzle-kit';

export default defineConfig({
  dialect: 'postgresql',   // 'mysql' | 'sqlite'
  schema: './src/schema.ts',
  out: './drizzle/migrations',
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
});

Bước 3: introspect database hiện tại — đừng bỏ qua bước này

npx drizzle-kit introspect

Lệnh này tạo file Drizzle schema từ các bảng database hiện có. Bước này bắt buộc phải làm trước khi generate bất kỳ migration mới nào. Drizzle Kit diff dựa trên trạng thái database thực tế. Nếu không có snapshot introspect làm baseline, nó không biết Prisma đã chạy gì — và sẽ tạo ra các lệnh CREATE TABLE trùng lặp khi bạn chạy generate lần đầu.

Sau khi introspect: commit file schema được tạo ra làm baseline. Mọi lần chạy drizzle-kit generate tiếp theo sẽ bắt đầu từ điểm đó.

Bước 4: chuyển đổi schema

Prisma schema:

model User {
  id        String   @id @default(cuid())
  email     String   @unique
  name      String?
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  posts     Post[]
}

Tương đương trong Drizzle (PostgreSQL):

import { pgTable, text, timestamp } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: text('id').primaryKey(),
  email: text('email').notNull().unique(),
  name: text('name'),
  createdAt: timestamp('created_at').notNull().defaultNow(),
  updatedAt: timestamp('updated_at').notNull().defaultNow(),  // không có @updatedAt tương đương — xem Cạm bẫy #3
});

export const posts = pgTable('posts', {
  id: text('id').primaryKey(),
  userId: text('user_id').notNull().references(() => users.id),
});

Relation được khai báo riêng, không suy ra từ foreign key:

import { relations } from 'drizzle-orm';

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

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

Bước 5: viết lại query

Tạo / Insert

// Prisma
await prisma.user.create({ data: { email, name } });
await prisma.user.createMany({ data: [{ email: 'a' }, { email: 'b' }] });

// Drizzle — array là native, không có method "createMany" riêng
await db.insert(users).values({ email, name });
await db.insert(users).values([{ email: 'a' }, { email: 'b' }]);

Đọc

// Prisma
const user = await prisma.user.findFirst({
  where: { email },
  include: { posts: true },
});

// Drizzle — relational API (cần khai báo relations ở trên)
const user = await db.query.users.findFirst({
  where: eq(users.email, email),
  with: { posts: true },
});

// Drizzle — core API (join tường minh, không cần định nghĩa relation)
const user = await db
  .select()
  .from(users)
  .leftJoin(posts, eq(posts.userId, users.id))
  .where(eq(users.email, email))
  .limit(1);

// Tìm kiếm không phân biệt hoa thường
const results = await db
  .select()
  .from(users)
  .where(ilike(users.name, '%al%'))
  .offset(0)
  .limit(20);

Aggregation

Các helper _count, _sum, _avg của Prisma không tồn tại trong relational query API của Drizzle. Dùng core API thay thế:

// Prisma
const agg = await prisma.post.aggregate({ _count: { id: true } });

// Drizzle
import { count } from 'drizzle-orm';
const [{ total }] = await db.select({ total: count() }).from(posts);

Với aggregation phức tạp hơn, dùng raw SQL có type safety:

const [{ revenue }] = await db
  .select({ revenue: sql<number>`sum(${orders.amount})` })
  .from(orders);

Cập nhật

// Prisma
await prisma.user.update({ where: { id }, data: { name } });

// Drizzle
await db.update(users).set({ name }).where(eq(users.id, id));

Xóa

// Prisma
await prisma.user.delete({ where: { id } });

// Drizzle
await db.delete(users).where(eq(users.id, id));

Bước 6: xử lý transaction

// Prisma — dạng array (Drizzle không có tương đương)
await prisma.$transaction([
  prisma.user.create(...),
  prisma.post.create(...),
]);

// Prisma — dạng callback (cái này ánh xạ tốt sang Drizzle)
await prisma.$transaction(async (tx) => { ... });

// Drizzle — chỉ có dạng callback
await db.transaction(async (tx) => {
  await tx.insert(users).values({ email });
  await tx.insert(auditLog).values({ action: 'user_created' });
});

Transaction dạng array của Prisma không có tương đương trong Drizzle. Mọi code dùng dạng array phải được refactor sang dạng callback trước khi migration.

Bước 7: migration về sau

Sau khi xóa Prisma:

# Tạo file migration từ thay đổi schema
npx drizzle-kit generate

# Áp dụng các migration đang chờ
npx drizzle-kit migrate

# Hoặc push thẳng lên database dev (không tạo file migration)
npx drizzle-kit push

Drizzle không tạo down-migration. Nếu bạn cần khả năng rollback, phải viết thủ công.

Các cạm bẫy cần chú ý

C1 — Xung đột lịch sử migration (bất ngờ lớn nhất)

Drizzle Kit không biết gì về lịch sử migration của Prisma. Nó diff dựa trên database thực tế. Nếu chạy drizzle-kit generate mà chưa chạy drizzle-kit introspect trước, nó sẽ cố tạo lại toàn bộ bảng từ đầu.

Luôn introspect trước, commit snapshot, rồi mới generate. Đây là bước mà hầu hết các bài hướng dẫn migration bỏ qua, và chính nó sẽ chặn CI của bạn.

Nguồn: DEV.to migration post, GitHub Discussions #2114.

C2 — Cột Decimal trả về dưới dạng string

Drizzle trả về các cột decimalnumeric dưới dạng JavaScript string để bảo toàn độ chính xác vượt quá giới hạn của number. Prisma trả về dưới dạng số. Phép tính số học trên các giá trị này cần chuyển đổi tường minh:

// Prisma — dùng trực tiếp được
product.price * quantity

// Drizzle — phải chuyển đổi trước
parseFloat(product.price) * quantity

Quét toàn bộ codebase để tìm phép tính số học trên các cột typed Decimal trong Prisma trước khi chuyển sang Drizzle.

C3 — Không có updatedAt tự động

@updatedAt của Prisma tự cập nhật timestamp mỗi khi có write. Drizzle không có tương đương. Bạn phải set thủ công trong mỗi lần update:

await db.update(users)
  .set({ name, updatedAt: new Date() })
  .where(eq(users.id, id));

Hoặc dùng database-level trigger. Dù cách nào bạn cũng phải tự cấu hình — Drizzle không làm thay.

C4 — UUID default hoạt động khác

@default(uuid()) của Prisma tạo UUID ở application layer. Trong Drizzle, dùng database-level generation cho PostgreSQL:

import { uuid } from 'drizzle-orm/pg-core';

id: uuid('id').primaryKey().defaultRandom(),  // gọi gen_random_uuid() trong PG

Hoặc tạo trong application code:

import { createId } from '@paralleldrive/cuid2';

id: text('id').primaryKey().$defaultFn(() => createId()),

C5 — Introspection có thể suy ra sai nullable column

Ít nhất một migration production gặp bug khi drizzle-kit introspect tạo nullable column cho field NOT NULL, dẫn đến lỗi type lúc runtime. Sau khi introspect, kiểm tra thủ công file schema được tạo trước khi commit — đặc biệt chú ý các cột không được phép null.

C6 — Đặc thù của Cloudflare D1

Nếu bạn đang migrate sang D1 cụ thể:

  • D1 dùng SQLite. Không có FTS5 full-text search; dùng Cloudflare Vectorize thay thế.
  • Bạn không thể kết nối đến D1 từ script Node.js local trực tiếp — dùng drizzle-kit với D1 HTTP API để chạy migration.
  • D1 có read replica có thể gây read-after-write inconsistency. Dùng .returning() trên insert để đọc từ primary.
  • Giới hạn 10 GB của D1 trên gói trả phí và thiết kế tối ưu cho read khiến nó không phù hợp cho logging hay event pipeline — kết hợp với Cloudflare Queues cho những việc đó.

Nếu bạn đang migrate sang D1, xem Turso vs Cloudflare D1 để so sánh trực tiếp hai lựa chọn SQLite tại edge.

C7 — Aggregation không hoạt động trong relational query builder

Relational API db.query.X.findMany({ ... }) của Drizzle không hỗ trợ COUNT, SUM, AVG, hay bất kỳ hàm tổng hợp nào. Chuyển sang core query builder cho mọi thứ cần aggregate:

// Cái này KHÔNG hoạt động
await db.query.orders.findMany({ _sum: { amount: true } });  // API của Prisma, không phải Drizzle

// Cái này hoạt động
const [{ total }] = await db
  .select({ total: sql<number>`sum(${orders.amount})` })
  .from(orders);

C8 — Không có migration rollback

Drizzle chỉ tạo up-migration. Không có down-migration tự động. Có feature request (GitHub Issue #2352) nhưng chưa có trong 0.45.2. Viết rollback thủ công nếu cần, hoặc thiết kế migration sao cho có thể đảo ngược.

Kết luận

Nên chuyển nếu:

  • Bạn nhắm đến Cloudflare Workers, D1, hoặc edge runtime có giới hạn bundle size nghiêm ngặt. So sánh 67 KB của Drizzle với 1.6 MB của Prisma — không phải trên server thông thường.
  • Cold start là vấn đề thực đo được ở edge. Một benchmark Vercel Edge đã công bố cho thấy cải thiện 4× sau migration (820 ms → 210 ms, xem tài liệu tham khảo số 5).
  • Team bạn quen SQL và thích API query không trừu tượng hóa join.
  • Bạn muốn loại bỏ codegen khỏi CI.
  • Bạn dùng SQLite/D1 (Prisma không hỗ trợ).

Nên ở lại Prisma nếu:

  • Bạn đang dùng Prisma 7 trên Lambda hoặc container thông thường. Khoảng cách hiệu năng không còn đủ để biện minh cho việc migration với hầu hết ứng dụng.
  • Team bạn viết nhiều query include lồng nhau mà không quen SQL. Join tường minh đòi hỏi nhiều code hơn cho cùng kết quả.
  • Bạn dùng MongoDB hoặc MSSQL. Drizzle không hỗ trợ.
  • Bạn phụ thuộc nhiều vào @updatedAt, uuid() default, hoặc helper _count/_sum. Mỗi cái cần thay thế thủ công — chi phí cộng dồn đáng kể với schema lớn.

Sau Prisma 7, lý do cold-start đơn thuần không còn đủ sức thuyết phục cho việc migration trên server thông thường. Lý do rõ ràng hơn để chuyển sang Drizzle là hỗ trợ Cloudflare D1, ưu tiên cú pháp kiểu SQL, hoặc loại bỏ codegen khỏi pipeline. Với ai đang chạy ổn định trên Prisma 7 và VPS hoặc container: chi phí chuyển đổi vượt quá lợi ích hiệu năng với hầu hết ứng dụng.

Tài liệu tham khảo

  1. Hướng dẫn migration chính thức của Drizzle từ Prisma
  2. Drizzle Prisma extension
  3. Tài liệu Drizzle Cloudflare D1
  4. Thread migration cộng đồng — 19 bảng, 21 giờ, giảm 25% số dòng code
  5. Migration SaaS thực tế (DEV.to) — 820 ms → 210 ms cold start
  6. Xung đột lịch sử migration — GitHub Discussions #2114
  7. Feature request rollback — GitHub Issue #2352
  8. Drizzle vs Prisma — so sánh định tính (encore.dev)