Tại sao Drizzle ORM đang dần thay thế Prisma trong các Modern TypeScript Stack

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

Vấn đề với các lớp trừu tượng cơ sở dữ liệu nặng nề

Trong nhiều năm, Prisma là lựa chọn mặc định của các lập trình viên TypeScript. Nó đã giải quyết bài toán quản lý cơ sở dữ liệu trong Node.js bằng auto-generated client và khả năng type safety cực kỳ vững chắc.

Nhưng khi kiến trúc serverless trở nên phổ biến, những nhược điểm bắt đầu lộ diện. Tôi đã từng thấy các Lambda function bị trì hoãn tới 2 giây do cold start, mà nguyên nhân chính là từ query engine viết bằng Rust khá nặng nề của Prisma. Khi lớp cơ sở dữ liệu gây ra độ trễ lớn như vậy trước khi code nghiệp vụ (business logic) bắt đầu chạy, đó là lúc chúng ta cần cân nhắc lại stack công nghệ.

Vấn đề nằm ở “khoảng cách trừu tượng” (abstraction gap). Nhiều ORM cố gắng che giấu SQL đằng sau một ngôn ngữ riêng (DSL). Điều này có vẻ ổn cho đến khi bạn cần thực hiện các câu lệnh complex join hoặc window function mà DSL không hỗ trợ. Lúc đó, bạn phải loay hoay đối phó với công cụ thay vì tập trung phát triển tính năng. Drizzle ORM đi theo một hướng khác: nó là một lớp mỏng, type-safe phủ trên SQL tiêu chuẩn. Nếu bạn biết viết câu lệnh SELECT cơ bản, bạn đã biết cách sử dụng Drizzle.

Quick Start: Từ con số 0 đến câu truy vấn đầu tiên trong 5 phút

Drizzle mang lại cảm giác mới mẻ vì nó không yêu cầu CLI toàn cục hay các bước khởi tạo phức tạp. Nó đơn thuần là một thư viện. Để dễ hình dung về kích thước: trong khi engine của Prisma có thể chiếm hơn 15MB trong gói deployment của bạn, Drizzle gần như không đáng kể với dung lượng chỉ khoảng 15KB.

Bắt đầu bằng cách cài đặt các gói cốt lõi cho PostgreSQL:

npm install drizzle-orm pg
npm install -D drizzle-kit @types/pg typescript

Định nghĩa cấu trúc dữ liệu trong file schema.ts. Cú pháp này mô phỏng các định nghĩa bảng SQL nhưng hoàn toàn nằm trong môi trường TypeScript:

import { pgTable, serial, text, varchar, timestamp } from "drizzle-orm/pg-core";

export const users = pgTable("users", {
  id: serial("id").primaryKey(),
  fullName: text("full_name").notNull(),
  email: varchar("email", { length: 255 }).unique().notNull(),
  createdAt: timestamp("created_at").defaultNow(),
});

Việc kết nối với cơ sở dữ liệu rất đơn giản. Drizzle sử dụng driver hiện có của bạn (như pg hoặc postgres.js) và cung cấp khả năng type safety mà không cần bước tạo code ngầm:

import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
import { users } from "./schema";

const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const db = drizzle(pool);

async function main() {
  const allUsers = await db.select().from(users);
  console.log("Số lượng người dùng:", allUsers.length);
}

Thiết kế Schema và Migration minh bạch

Drizzle tuân theo triết lý “SQL-first”. Bạn không định nghĩa các đối tượng trừu tượng vốn có thể hiển thị khác đi trong cơ sở dữ liệu. Thay vào đó, bạn định nghĩa trực tiếp trạng thái của DB. Sự minh bạch này giúp việc debug trở nên dễ dàng hơn đáng kể.

Định nghĩa các mối quan hệ

Các mối quan hệ trong Drizzle được thể hiện rõ ràng (explicit). Điều này giúp cơ sở dữ liệu không trở thành một “hộp đen”. Bạn định nghĩa các khóa ngoại (foreign keys) giống như trong script migration, đảm bảo tính toàn vẹn dữ liệu ở cấp độ phần cứng.

import { integer, pgTable, serial, text } from "drizzle-orm/pg-core";

export const posts = pgTable("posts", {
  id: serial("id").primaryKey(),
  title: text("title").notNull(),
  authorId: integer("author_id").references(() => users.id),
});

Tự động hóa SQL với Drizzle Kit

Xử lý migration thường là phần căng thẳng nhất khi triển khai. Drizzle Kit đơn giản hóa việc này bằng cách so sánh schema TypeScript với cơ sở dữ liệu thực tế của bạn, sau đó tạo ra các file SQL tiêu chuẩn. Không giống như các công cụ khác sử dụng định dạng JSON hoặc XML riêng biệt, Drizzle cung cấp mã SQL thuần túy mà bạn có thể đọc, chỉnh sửa và quản lý phiên bản.

Trong môi trường production, khả năng hiển thị này là một điểm cộng lớn. Bạn có thể thấy chính xác các lệnh ALTER TABLE nào sẽ chạy trước khi chúng tác động đến dữ liệu. Để tạo một migration, chỉ cần chạy:

npx drizzle-kit generate

Lệnh này sẽ tạo ra một file .sql. Bạn có thể áp dụng nó bằng lệnh migrate trong pipeline CI/CD hoặc sử dụng npx drizzle-kit push để tạo prototype nhanh chóng trong môi trường phát triển local.

Truy vấn tối ưu và Prepared Statements

Mặc dù Drizzle thế mạnh ở cú pháp giống SQL, nó cũng cung cấp API “Relational Queries” để nâng cao trải nghiệm lập trình. API này được xây dựng nhằm giải quyết vấn đề truy vấn N+1 bằng cách lấy dữ liệu lồng nhau chỉ trong một câu truy vấn SQL duy nhất và được tối ưu hóa cao.

const usersWithPosts = await db.query.users.findMany({
  with: {
    posts: true,
  },
});

Nếu bạn cần hiệu năng tối đa cho các endpoint có lưu lượng truy cập cao, Drizzle hỗ trợ **Prepared Statements**. Chúng cho phép cơ sở dữ liệu biên dịch trước kế hoạch thực thi. Theo thử nghiệm của tôi, việc sử dụng prepared statements có thể giảm 10-15% độ trễ truy vấn bằng cách bỏ qua giai đoạn phân tích cho các yêu cầu lặp lại.

const userQuery = db.select().from(users).where(eq(users.id, placeholder('id'))).prepare('userQuery');
await userQuery.execute({ id: 1 });

Lời khuyên chuyên gia khi triển khai Production

Chuyển sang Drizzle đòi hỏi một chút thay đổi về tư duy nếu bạn đã quen với các ORM “ma thuật”. Dưới đây là cách giữ cho codebase sạch sẽ khi mở rộng:

  • Tích hợp Zod sớm: Sử dụng drizzle-zod để tạo các validation schema trực tiếp từ định nghĩa bảng. Điều này giúp các kiểu dữ liệu của API và database luôn đồng bộ hoàn hảo mà không cần cập nhật thủ công.
  • Chia nhỏ các bảng: Đừng nhồi nhét 50 bảng vào một file duy nhất. Hãy tạo thư mục schema/ với các file riêng biệt cho từng lĩnh vực (ví dụ: billing.ts, auth.ts) và re-export chúng từ một file index trung tâm.
  • Sử dụng Strict Mode: Kích hoạt strict: true trong drizzle.config.ts. Nó bắt buộc bạn phải xử lý các trường hợp ngoại lệ và ngăn chặn việc mất dữ liệu ngoài ý muốn khi thay đổi schema.
  • SQL Template Literal: Khi bạn cần tính năng cụ thể của PostgreSQL như điều hướng đường dẫn JSONB, hãy sử dụng toán tử sql. Nó cho phép bạn viết các đoạn mã SQL thuần túy nhưng vẫn đảm bảo type-safe và có khả năng kết hợp.

Drizzle ORM tạo ra một sự cân bằng hiên có: nó tôn trọng kiến thức SQL của bạn trong khi vẫn mang lại trải nghiệm TypeScript đẳng cấp. Nếu bạn đã mệt mỏi với việc đối phó với cold start chậm chạp hoặc vật lộn với các DSL phức tạp, Drizzle là bước tiến logic tiếp theo. Nó nhanh, dễ dự đoán và luôn hỗ trợ bạn để bạn có thể tập trung vào việc xây dựng tính năng.

Share: