So sánh các cách tương tác Database trong TypeScript
Khi build backend với PostgreSQL và TypeScript, anh em thường phân vân giữa 3 trường phái chính:
- Raw SQL / Query Builder (pg, Knex.js): Viết SQL thuần cho hiệu năng chạm nóc. Bạn kiểm soát 100% câu query gửi xuống DB. Đổi lại, việc map type TypeScript thủ công rất dễ sót. Một lỗi typo nhỏ ở tên cột cũng đủ làm sập API trên production.
- Heavy ORM (TypeORM, Prisma): Prisma mang lại DX (Developer Experience) mượt mà với auto-generated types. Nhưng điểm trừ lớn nằm ở Rust query engine nặng từ 15–30MB. Trên AWS Lambda hoặc Vercel Serverless, engine này có thể kéo cold start lên tới 300ms–1s, đồng thời đôi khi sinh ra các câu query lồng nhau khó kiểm soát.
- Lightweight Type-safe ORM (Drizzle ORM): Đi theo triết lý “If you know SQL, you know Drizzle”. Không engine cồng kềnh. Không runtime magic. Drizzle chỉ nặng khoảng ~30KB, dịch trực tiếp cú pháp TypeScript sang SQL 1-1 và giữ trọn type-safety từ schema đến kết quả trả về.
Ưu và nhược điểm thực tế của Drizzle ORM
Trong một dự án xử lý hơn 2.000 req/s gần đây, team mình quyết định chuyển đổi từ Prisma sang Drizzle ORM trên cụm PostgreSQL (RDS db.t4g.medium). Dưới đây là những gì team rút ra sau vài tháng chạy production:
Điểm cộng lớn
- Khởi động tức thì (Zero Engine Overhead): Bundle size siêu nhẹ giúp giảm cold start trên Serverless và Cloudflare Workers xuống dưới 15ms.
- Cú pháp sát với SQL gốc: Bạn không cần học thuộc hàng tá abstraction method mới. Biết SQL là viết được Drizzle.
- Tự suy luận Type chuẩn xác: Type của kết quả query tự động ăn theo schema. Bạn không cần chạy lệnh build hay generate code trung gian mỗi khi sửa DB.
- Bộ công cụ Drizzle Kit tiện lợi: Hỗ trợ diff schema tự động để gen file migration, kèm Drizzle Studio trực quan giúp xem data ngay trên trình duyệt mà không cần cài thêm DBeaver hay pgAdmin.
Điểm trừ cần cân nhắc
- Hệ sinh thái và cộng đồng StackOverflow chưa dày dặn bằng Prisma hay TypeORM. Gặp lỗi hiếm đôi khi phải tự đọc source code trên GitHub.
- Với các quan hệ n-n phức tạp, bạn vẫn cần tư duy SQL vững để viết join hợp lý thay vì phó mặc hoàn toàn cho ORM.
Khi nào nên chọn Drizzle ORM?
Drizzle sẽ phát huy tối đa sức mạnh trong các kịch bản sau:
- Ứng dụng chạy trên Edge Functions, Cloudflare Workers hoặc AWS Lambda cần thời gian khởi động tối thiểu.
- Team thích viết SQL tường minh, muốn kiểm soát chính xác từng câu query gửi xuống database.
- Dự án đòi hỏi tối ưu RAM và CPU server nhưng vẫn muốn code TypeScript an toàn tuyệt đối về kiểu dữ liệu.
Hướng dẫn triển khai Drizzle ORM với PostgreSQL
Bước 1: Khởi tạo dự án và cài đặt package
Đầu tiên, khởi tạo project và cài đặt Drizzle cùng driver postgres (postgres.js):
mkdir drizzle-pg-demo && cd drizzle-pg-demo
npm init -y
npm install drizzle-orm postgres dotenv
npm install -D typescript @types/node drizzle-kit tsx
npx tsc --init
Bước 2: Cấu hình kết nối Database
Tạo file .env để lưu connection string:
DATABASE_URL="postgres://postgres:password@localhost:5432/my_db"
Tiếp theo, tạo file khởi tạo client tại src/db/index.ts:
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';
import 'dotenv/config';
const connectionString = process.env.DATABASE_URL!;
// Khởi tạo client postgres-js với connection pool
const client = postgres(connectionString, { max: 10 });
export const db = drizzle(client, { schema });
Bước 3: Định nghĩa Schema với Type-safety
Tạo file src/db/schema.ts. Ở đây chúng ta khai báo hai bảng users và posts có quan hệ 1-n:
import { pgTable, serial, text, timestamp, integer } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
export const users = pgTable('users', {
id: serial('id').primaryKey(),
fullName: text('full_name').notNull(),
email: text('email').notNull().unique(),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: text('title').notNull(),
content: text('content'),
authorId: integer('author_id')
.references(() => users.id, { onDelete: 'cascade' })
.notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
// Định nghĩa relations để query dạng lồng nhau
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
}));
// Trích xuất TypeScript Types trực tiếp từ Schema
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
export type Post = typeof posts.$inferSelect;
export type NewPost = typeof posts.$inferInsert;
Bước 4: Cấu hình Drizzle Kit và đồng bộ Schema
Tạo file cấu hình drizzle.config.ts tại thư mục gốc:
import { defineConfig } from 'drizzle-kit';
import 'dotenv/config';
export default defineConfig({
schema: './src/db/schema.ts',
out: './drizzle',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});
Đẩy trực tiếp schema vào DB trong môi trường local:
# Sync schema trực tiếp vào PostgreSQL (phù hợp dev local)
npx drizzle-kit push
# Mở GUI quản lý data trực quan trên port 4983
npx drizzle-kit studio
Lưu ý: Với môi trường production, hãy dùng
npx drizzle-kit generateđể tạo file SQL migration và chạymigrate()qua CI/CD pipeline nhằm kiểm soát lịch sử thay đổi.
Bước 5: Thao tác CRUD thực tế
Tạo file src/index.ts để test các thao tác insert, select và relational query:
import { db } from './db';
import { users, posts } from './db/schema';
import { eq, desc } from 'drizzle-orm';
async function main() {
// 1. Insert User mới và nhận ngay record trả về
const [newUser] = await db.insert(users).values({
fullName: 'Nguyen Van A',
email: `vana_${Date.now()}@example.com`,
}).returning();
console.log('Inserted User:', newUser);
// 2. Insert Post liên kết với User
await db.insert(posts).values({
title: 'Học Drizzle ORM trong 10 phút',
content: 'Drizzle ORM mang lại trải nghiệm SQL thuần cực mượt.',
authorId: newUser.id,
});
// 3. Query theo cú pháp SQL chuẩn
const allUsers = await db.select().from(users).orderBy(desc(users.createdAt)).limit(5);
console.log('Top 5 Users:', allUsers);
// 4. Relational Query (Lấy User kèm toàn bộ danh sách Posts)
const usersWithPosts = await db.query.users.findMany({
where: eq(users.id, newUser.id),
with: {
posts: true,
},
});
console.log('User with posts:', JSON.stringify(usersWithPosts, null, 2));
}
main().catch(console.error);
Chạy thử nghiệm:
npx tsx src/index.ts
3 kinh nghiệm thực chiến khi đưa Drizzle lên Production
- Kiểm soát Connection Pool: Khi deploy lên serverless, mỗi instance có thể mở một pool riêng khiến PostgreSQL cạn kiệt connection (lỗi
too many clients already). Hãy dùng giải pháp connection pooling như PgBouncer, Supabase Pooler hoặc Neon Serverless Driver. - Tận dụng triệt để
.returning(): Đừng tốn thêm một câu SELECT sau khi INSERT hoặc UPDATE. Mệnh đềRETURNING *của PostgreSQL cho phép bạn lấy dữ liệu mới nhất ngay trong một round-trip mạng. - Luôn export
$inferSelectvà$inferInsert: Sử dụng các type này làm chuẩn cho DTO hoặc API response. Khi schema thay đổi, TypeScript compiler sẽ cảnh báo ngay lập tức ở các tầng code liên quan.

