ORM Guide: Prisma vs TypeORM vs Drizzle

Giới thiệu
Mọi backend TypeScript project đều đối mặt cùng một câu hỏi: làm sao nói chuyện với database? Raw SQL mạnh nhưng dễ lỗi. Query builders linh hoạt nhưng dài dòng. ORMs hứa hẹn productivity nhưng đôi khi giấu quá nhiều thứ.
Hệ sinh thái TypeScript có ba ORM lớn, mỗi cái mang triết lý khác nhau hoàn toàn: Prisma (schema-first, auto-generated client), TypeORM (decorator-based, ActiveRecord/DataMapper), và Drizzle (SQL-like, zero abstraction). Chọn sai có thể tốn hàng tháng refactoring.
Bài viết này cho bạn bức tranh toàn cảnh để đưa ra lựa chọn đúng cho project.
Bạn sẽ học được gì
✅ ORM là gì và khi nào thực sự cần dùng
✅ Architecture của Prisma: schema-first design, Prisma Client, và migrations
✅ Architecture của TypeORM: decorators, ActiveRecord vs DataMapper patterns
✅ Architecture của Drizzle: SQL-like syntax với full type safety
✅ So sánh song song queries, relations, migrations, và performance
✅ Các tình huống thực tế: ORM nào phù hợp loại project nào
✅ Chiến lược migration giữa các ORMs
Yêu cầu
- Nắm vững TypeScript (TypeScript Full-Stack Roadmap)
- Kiến thức SQL cơ bản (SQL Fundamentals)
- Hiểu khái niệm relational database (Relational Database Fundamentals)
1. Hiểu về ORMs
ORM là gì?
ORM (Object-Relational Mapping) ánh xạ database tables sang objects trong ngôn ngữ lập trình, để bạn viết code thay vì SQL.
Phổ Database Access
| Cách tiếp cận | Type Safety | Learning Curve | SQL Control | Productivity |
|---|---|---|---|---|
Raw SQL (pg, mysql2) | ❌ Không | Thấp | Toàn bộ | Thấp |
| Query Builder (Knex.js) | ⚠️ Một phần | Trung bình | Cao | Trung bình |
| Drizzle ORM | ✅ Đầy đủ | Thấp-TB | Cao | Cao |
| TypeORM | ✅ Đầy đủ | TB-Cao | Trung bình | Cao |
| Prisma | ✅ Đầy đủ | Thấp | Thấp-TB | Rất cao |
Khi nào cần ORM?
Dùng ORM khi:
- Bạn muốn database queries type-safe
- App có quan hệ phức tạp (1:N, M:N)
- Bạn cần quản lý migrations
- Bạn ưu tiên developer productivity hơn SQL control
Bỏ qua ORM khi:
- Queries cần performance-critical cần hand-tuned SQL
- Bạn đang xây script đơn giản với vài queries
- Team thích viết SQL trực tiếp
2. Prisma: Schema-First ORM
Triết lý
Prisma dùng cách tiếp cận schema-first. Bạn định nghĩa data model trong file .prisma, và Prisma generate type-safe client hoàn toàn. Bạn không bao giờ viết SQL hay decorators — schema là single source of truth.
Thiết lập
npm install prisma @prisma/client
npx prisma initTạo ra:
prisma/
├── schema.prisma # Định nghĩa data model
└── migrations/ # Lịch sử migrationĐịnh nghĩa Schema
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
role Role @default(USER)
posts Post[]
profile Profile?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([email])
@@map("users")
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
categories Category[]
createdAt DateTime @default(now())
@@index([authorId])
@@map("posts")
}
model Profile {
id Int @id @default(autoincrement())
bio String
user User @relation(fields: [userId], references: [id])
userId Int @unique
@@map("profiles")
}
model Category {
id Int @id @default(autoincrement())
name String @unique
posts Post[]
@@map("categories")
}
enum Role {
USER
ADMIN
MODERATOR
}Thao tác CRUD
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// CREATE
const user = await prisma.user.create({
data: {
email: '[email protected]',
name: 'John Doe',
profile: {
create: { bio: 'Full-stack developer' } // Nested create
}
},
include: { profile: true } // Kèm relation trong response
});
// READ với filtering
const publishedPosts = await prisma.post.findMany({
where: {
published: true,
author: {
role: 'ADMIN' // Filter qua relation
}
},
include: {
author: { select: { name: true, email: true } },
categories: true
},
orderBy: { createdAt: 'desc' },
take: 10,
skip: 0
});
// UPDATE
const updated = await prisma.user.update({
where: { email: '[email protected]' },
data: { role: 'ADMIN' }
});
// DELETE với cascade
const deleted = await prisma.user.delete({
where: { id: 1 }
});
// UPSERT
const upserted = await prisma.user.upsert({
where: { email: '[email protected]' },
update: { name: 'John Updated' },
create: { email: '[email protected]', name: 'John Doe' }
});Relations và Nested Queries
// Tạo post với categories (M:N)
const post = await prisma.post.create({
data: {
title: 'TypeScript ORMs',
content: 'So sánh Prisma, TypeORM, và Drizzle...',
author: { connect: { id: 1 } },
categories: {
connectOrCreate: [
{
where: { name: 'TypeScript' },
create: { name: 'TypeScript' }
},
{
where: { name: 'Database' },
create: { name: 'Database' }
}
]
}
},
include: { categories: true, author: true }
});
// Aggregate queries
const stats = await prisma.post.aggregate({
_count: { id: true },
_avg: { authorId: true },
where: { published: true }
});
// Group by
const postsByAuthor = await prisma.post.groupBy({
by: ['authorId'],
_count: { id: true },
having: {
id: { _count: { gt: 5 } }
}
});Migrations
# Tạo migration từ schema changes
npx prisma migrate dev --name add_user_role
# Apply migrations trong production
npx prisma migrate deploy
# Reset database (destructive)
npx prisma migrate reset
# Generate client không cần migration
npx prisma generateĐiểm mạnh và yếu của Prisma
| Điểm mạnh | Điểm yếu |
|---|---|
| Type safety tốt nhất | Không viết raw SQL type-safe được |
| API trực quan — dễ học | Prisma Client thêm bundle size |
| Prisma Studio (visual DB browser) | Schema language là proprietary |
| Quản lý migration xuất sắc | Complex raw queries cần $queryRaw |
| Types auto-generated, luôn in-sync | Joins thực hiện ở application level |
| Documentation tuyệt vời | Một số SQL patterns không làm native được |
3. TypeORM: Decorator-Based ORM
Triết lý
TypeORM theo pattern ORM truyền thống lấy cảm hứng từ Java's Hibernate và C#'s Entity Framework. Bạn định nghĩa entities bằng TypeScript decorators và chọn giữa ActiveRecord hoặc DataMapper patterns.
Thiết lập
npm install typeorm reflect-metadata @types/node pg// tsconfig.json — cài đặt bắt buộc
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"strictPropertyInitialization": false
}
}Cấu hình Data Source
// data-source.ts
import { DataSource } from 'typeorm';
import { User } from './entities/User';
import { Post } from './entities/Post';
import { Profile } from './entities/Profile';
import { Category } from './entities/Category';
export const AppDataSource = new DataSource({
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'postgres',
password: 'password',
database: 'myapp',
synchronize: false, // Không bao giờ true trong production!
logging: true,
entities: [User, Post, Profile, Category],
migrations: ['./migrations/*.ts']
});Định nghĩa Entity
// entities/User.ts
import {
Entity, PrimaryGeneratedColumn, Column, OneToMany,
OneToOne, CreateDateColumn, UpdateDateColumn, Index
} from 'typeorm';
import { Post } from './Post';
import { Profile } from './Profile';
export enum Role {
USER = 'USER',
ADMIN = 'ADMIN',
MODERATOR = 'MODERATOR'
}
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
@Index()
email: string;
@Column({ nullable: true })
name: string;
@Column({ type: 'enum', enum: Role, default: Role.USER })
role: Role;
@OneToMany(() => Post, (post) => post.author)
posts: Post[];
@OneToOne(() => Profile, (profile) => profile.user)
profile: Profile;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}// entities/Post.ts
import {
Entity, PrimaryGeneratedColumn, Column, ManyToOne,
ManyToMany, JoinTable, CreateDateColumn, Index
} from 'typeorm';
import { User } from './User';
import { Category } from './Category';
@Entity('posts')
export class Post {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@Column({ type: 'text', nullable: true })
content: string;
@Column({ default: false })
published: boolean;
@ManyToOne(() => User, (user) => user.posts)
@Index()
author: User;
@Column()
authorId: number;
@ManyToMany(() => Category, (category) => category.posts)
@JoinTable()
categories: Category[];
@CreateDateColumn()
createdAt: Date;
}// entities/Profile.ts
import {
Entity, PrimaryGeneratedColumn, Column,
OneToOne, JoinColumn
} from 'typeorm';
import { User } from './User';
@Entity('profiles')
export class Profile {
@PrimaryGeneratedColumn()
id: number;
@Column()
bio: string;
@OneToOne(() => User, (user) => user.profile)
@JoinColumn()
user: User;
@Column()
userId: number;
}Thao tác CRUD (DataMapper Pattern)
const userRepo = AppDataSource.getRepository(User);
const postRepo = AppDataSource.getRepository(Post);
// CREATE
const user = userRepo.create({
email: '[email protected]',
name: 'John Doe'
});
await userRepo.save(user);
// CREATE với relation
const profile = new Profile();
profile.bio = 'Full-stack developer';
profile.user = user;
await AppDataSource.getRepository(Profile).save(profile);
// READ với filtering
const publishedPosts = await postRepo.find({
where: {
published: true,
author: { role: Role.ADMIN }
},
relations: { author: true, categories: true },
order: { createdAt: 'DESC' },
take: 10,
skip: 0
});
// UPDATE
await userRepo.update(
{ email: '[email protected]' },
{ role: Role.ADMIN }
);
// DELETE
await userRepo.delete({ id: 1 });
// UPSERT (TypeORM 0.3+)
await userRepo.upsert(
{ email: '[email protected]', name: 'John Updated' },
['email']
);QueryBuilder cho Queries phức tạp
// TypeORM's QueryBuilder cho advanced queries
const posts = await postRepo
.createQueryBuilder('post')
.innerJoinAndSelect('post.author', 'author')
.leftJoinAndSelect('post.categories', 'category')
.where('post.published = :published', { published: true })
.andWhere('author.role IN (:...roles)', { roles: ['ADMIN', 'MODERATOR'] })
.orderBy('post.createdAt', 'DESC')
.take(10)
.skip(0)
.getMany();
// Aggregate với QueryBuilder
const stats = await postRepo
.createQueryBuilder('post')
.select('post.authorId', 'authorId')
.addSelect('COUNT(post.id)', 'postCount')
.where('post.published = true')
.groupBy('post.authorId')
.having('COUNT(post.id) > :minPosts', { minPosts: 5 })
.getRawMany();
// Subqueries
const usersWithPosts = await userRepo
.createQueryBuilder('user')
.where((qb) => {
const subQuery = qb
.subQuery()
.select('post.authorId')
.from(Post, 'post')
.where('post.published = true')
.getQuery();
return `user.id IN ${subQuery}`;
})
.getMany();Migrations
# Generate migration từ entity changes
npx typeorm migration:generate ./migrations/AddUserRole -d ./data-source.ts
# Tạo migration trống
npx typeorm migration:create ./migrations/SeedData
# Chạy migrations
npx typeorm migration:run -d ./data-source.ts
# Revert migration cuối
npx typeorm migration:revert -d ./data-source.tsĐiểm mạnh và yếu của TypeORM
| Điểm mạnh | Điểm yếu |
|---|---|
| Quen thuộc với dev Java/C# | Decorator syntax dài dòng |
| ActiveRecord + DataMapper patterns | Type safety gaps (relations có thể undefined) |
| QueryBuilder mạnh mẽ | synchronize: true có thể phá hủy data production |
| Hỗ trợ nhiều databases | Release cycle chậm, nhiều issues mở |
| Hệ sinh thái trưởng thành | Entity metadata cần reflect-metadata |
| Transaction support xuất sắc | Setup phức tạp với yêu cầu tsconfig |
4. Drizzle: SQL-Like ORM
Triết lý
Drizzle theo cách tiếp cận "nếu bạn biết SQL, bạn biết Drizzle". Đây là lớp mỏng, type-safe trên SQL mà không giấu queries bên dưới. Bạn định nghĩa schemas trong TypeScript (không decorators, không proprietary languages), và queries trông giống SQL.
Thiết lập
npm install drizzle-orm postgres
npm install -D drizzle-kitĐịnh nghĩa Schema
// db/schema.ts
import {
pgTable, serial, varchar, text, boolean, integer,
timestamp, pgEnum, uniqueIndex, index
} from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
// Enum
export const roleEnum = pgEnum('role', ['USER', 'ADMIN', 'MODERATOR']);
// Tables
export const users = pgTable('users', {
id: serial('id').primaryKey(),
email: varchar('email', { length: 255 }).notNull().unique(),
name: varchar('name', { length: 255 }),
role: roleEnum('role').default('USER').notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
}, (table) => [
uniqueIndex('email_idx').on(table.email),
]);
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: varchar('title', { length: 255 }).notNull(),
content: text('content'),
published: boolean('published').default(false).notNull(),
authorId: integer('author_id').notNull().references(() => users.id),
createdAt: timestamp('created_at').defaultNow().notNull(),
}, (table) => [
index('author_idx').on(table.authorId),
]);
export const profiles = pgTable('profiles', {
id: serial('id').primaryKey(),
bio: text('bio').notNull(),
userId: integer('user_id').notNull().unique().references(() => users.id),
});
export const categories = pgTable('categories', {
id: serial('id').primaryKey(),
name: varchar('name', { length: 100 }).notNull().unique(),
});
// Bảng junction cho Many-to-many
export const postsToCategories = pgTable('posts_categories', {
postId: integer('post_id').notNull().references(() => posts.id),
categoryId: integer('category_id').notNull().references(() => categories.id),
}, (table) => [
index('post_category_idx').on(table.postId, table.categoryId),
]);
// Relations (cho query API)
export const usersRelations = relations(users, ({ many, one }) => ({
posts: many(posts),
profile: one(profiles, {
fields: [users.id],
references: [profiles.userId],
}),
}));
export const postsRelations = relations(posts, ({ one, many }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
categories: many(postsToCategories),
}));
export const postsToCategoriesRelations = relations(postsToCategories, ({ one }) => ({
post: one(posts, {
fields: [postsToCategories.postId],
references: [posts.id],
}),
category: one(categories, {
fields: [postsToCategories.categoryId],
references: [categories.id],
}),
}));Kết nối Database
// db/index.ts
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';
const client = postgres(process.env.DATABASE_URL!);
export const db = drizzle(client, { schema });Thao tác CRUD
import { eq, and, desc, sql, inArray, gt, count } from 'drizzle-orm';
import { db } from './db';
import { users, posts, profiles, categories, postsToCategories } from './db/schema';
// CREATE
const [user] = await db.insert(users).values({
email: '[email protected]',
name: 'John Doe',
}).returning();
// CREATE với relation (insert riêng)
await db.insert(profiles).values({
bio: 'Full-stack developer',
userId: user.id,
});
// READ với filtering (SQL-like API)
const publishedPosts = await db
.select()
.from(posts)
.innerJoin(users, eq(posts.authorId, users.id))
.where(
and(
eq(posts.published, true),
inArray(users.role, ['ADMIN', 'MODERATOR'])
)
)
.orderBy(desc(posts.createdAt))
.limit(10)
.offset(0);
// READ với relations (Query API)
const usersWithPosts = await db.query.users.findMany({
with: {
posts: {
where: eq(posts.published, true),
limit: 5,
},
profile: true,
},
});
// UPDATE
await db.update(users)
.set({ role: 'ADMIN' })
.where(eq(users.email, '[email protected]'));
// DELETE
await db.delete(users).where(eq(users.id, 1));
// UPSERT (ON CONFLICT)
await db.insert(users)
.values({ email: '[email protected]', name: 'John Updated' })
.onConflictDoUpdate({
target: users.email,
set: { name: 'John Updated' },
});Queries nâng cao
// Aggregate queries — trông giống SQL
const stats = await db
.select({
authorId: posts.authorId,
postCount: count(posts.id),
})
.from(posts)
.where(eq(posts.published, true))
.groupBy(posts.authorId)
.having(gt(count(posts.id), 5));
// Subqueries
const activeAuthors = db
.select({ authorId: posts.authorId })
.from(posts)
.where(eq(posts.published, true))
.as('active_authors');
const usersWithPublished = await db
.select()
.from(users)
.innerJoin(activeAuthors, eq(users.id, activeAuthors.authorId));
// Raw SQL khi cần (vẫn type-safe)
const result = await db.execute(
sql`SELECT * FROM users WHERE email ILIKE ${'%@example.com'}`
);
// Transactions
await db.transaction(async (tx) => {
const [newUser] = await tx.insert(users).values({
email: '[email protected]',
name: 'Jane',
}).returning();
await tx.insert(posts).values({
title: 'First Post',
authorId: newUser.id,
});
});Migrations
// drizzle.config.ts
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: './db/schema.ts',
out: './drizzle',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});# Generate migration từ schema changes
npx drizzle-kit generate
# Apply migrations
npx drizzle-kit migrate
# Push schema trực tiếp (chỉ dev)
npx drizzle-kit push
# Mở Drizzle Studio (visual DB browser)
npx drizzle-kit studioĐiểm mạnh và yếu của Drizzle
| Điểm mạnh | Điểm yếu |
|---|---|
| SQL-like syntax — không abstraction leak | Relations API tách biệt khỏi SQL API |
| Bundle size nhỏ nhất (~50KB) | Hệ sinh thái trẻ, ít tutorials |
| Không cần code generation | M:N relations cần junction tables thủ công |
| Chạy mọi runtime (Node, Bun, Deno) | Không có ActiveRecord pattern |
| Schema là TypeScript thuần | Relational queries cần định nghĩa relation rõ ràng |
| Drizzle Studio cho visual browsing | Ít "magic" — boilerplate nhiều hơn cho relations |
5. So sánh song song
Định nghĩa Schema
| Tính năng | Prisma | TypeORM | Drizzle |
|---|---|---|---|
| Ngôn ngữ | .prisma DSL | TypeScript decorators | TypeScript functions |
| Vị trí | schema.prisma | Entity classes | Schema files |
| Relations | Implicit qua fields | Decorators (@OneToMany) | Explicit relations() |
| Enums | enum trong schema | TypeScript enum + decorator | pgEnum() |
| Indexes | @@index | @Index() | Table callback |
| Validation | Schema-level | Decorator-based | Schema-level |
So sánh Query API
Tìm đơn giản theo ID:
// Prisma
const user = await prisma.user.findUnique({
where: { id: 1 }
});
// TypeORM
const user = await userRepo.findOneBy({ id: 1 });
// Drizzle
const [user] = await db.select().from(users).where(eq(users.id, 1));
// hoặc với query API:
const user = await db.query.users.findFirst({
where: eq(users.id, 1)
});Tìm với relations:
// Prisma — nested include
const user = await prisma.user.findUnique({
where: { id: 1 },
include: {
posts: { where: { published: true } },
profile: true
}
});
// TypeORM — relations option
const user = await userRepo.findOne({
where: { id: 1 },
relations: { posts: true, profile: true }
});
// hoặc QueryBuilder để filter relations:
const user = await userRepo
.createQueryBuilder('user')
.leftJoinAndSelect('user.posts', 'post', 'post.published = true')
.leftJoinAndSelect('user.profile', 'profile')
.where('user.id = :id', { id: 1 })
.getOne();
// Drizzle — query API
const user = await db.query.users.findFirst({
where: eq(users.id, 1),
with: {
posts: { where: eq(posts.published, true) },
profile: true
}
});Aggregation phức tạp:
// Prisma
const result = await prisma.post.groupBy({
by: ['authorId'],
_count: { id: true },
_avg: { authorId: true },
where: { published: true },
having: { id: { _count: { gt: 5 } } }
});
// TypeORM (QueryBuilder)
const result = await postRepo
.createQueryBuilder('post')
.select('post.authorId', 'authorId')
.addSelect('COUNT(post.id)', 'postCount')
.where('post.published = true')
.groupBy('post.authorId')
.having('COUNT(post.id) > :min', { min: 5 })
.getRawMany();
// Drizzle
const result = await db
.select({
authorId: posts.authorId,
postCount: count(posts.id),
})
.from(posts)
.where(eq(posts.published, true))
.groupBy(posts.authorId)
.having(gt(count(posts.id), 5));Ma trận tính năng
| Tính năng | Prisma | TypeORM | Drizzle |
|---|---|---|---|
| Type Safety | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| Learning Curve | Dễ | TB-Khó | Dễ-TB |
| Bundle Size | ~10MB (engine) | ~2MB | ~50KB |
| Raw SQL Support | $queryRaw | query() | sql template |
| Migration Tool | Built-in | Built-in | Drizzle Kit |
| Visual Studio | Prisma Studio | — | Drizzle Studio |
| Databases | PG, MySQL, SQLite, MongoDB, CockroachDB, SQL Server | PG, MySQL, SQLite, SQL Server, Oracle, MongoDB | PG, MySQL, SQLite, Turso |
| Code Generation | Bắt buộc (prisma generate) | Không cần | Không cần |
| Nested Creates | ✅ Built-in | ❌ Thủ công | ❌ Thủ công |
| ActiveRecord | ❌ | ✅ | ❌ |
| DataMapper | ✅ (mặc định) | ✅ | ✅ (mặc định) |
| Transactions | $transaction() | transaction() | transaction() |
| Edge/Serverless | ✅ (Prisma Accelerate) | ⚠️ Hạn chế | ✅ Native |
| Runtime Support | Node.js (Bun một phần) | Node.js | Node, Bun, Deno |
So sánh Migration
| Khía cạnh | Prisma | TypeORM | Drizzle |
|---|---|---|---|
| Generation | Từ schema diff | Từ entity diff | Từ schema diff |
| Format | SQL files | TypeScript classes | SQL files |
| Apply | prisma migrate deploy | migration:run | drizzle-kit migrate |
| Revert | Thủ công | migration:revert | Thủ công |
| Dev mode | prisma migrate dev | synchronize: true (nguy hiểm!) | drizzle-kit push |
| Seed | prisma db seed | Script tùy chỉnh | Script tùy chỉnh |
| An toàn | ✅ Cảnh báo data loss | ⚠️ synchronize có thể drop tables | ✅ Push có cảnh báo |
6. Đặc tính Performance
Query Generation
Prisma:
TypeORM:
Drizzle:
| Metric | Prisma | TypeORM | Drizzle |
|---|---|---|---|
| Cold Start | Chậm (engine init) | Trung bình | Nhanh |
| Query Overhead | Cao hơn (Rust engine IPC) | Trung bình (JS query building) | Tối thiểu (thin layer) |
| Connection Pooling | Built-in | Qua driver | Qua driver |
| Serverless | Cần Prisma Accelerate | Vấn đề cold start | Tốt nhất (bundle nhỏ nhất) |
| Chống N+1 | include giải quyết | relations thủ công | with trong query API |
| Batch Operations | createMany, updateMany | save(array), QueryBuilder | insert().values([...]) |
Serverless & Edge
Cho serverless/edge: Drizzle thắng nhờ bundle size tối thiểu và không phụ thuộc engine. Prisma cần Prisma Accelerate (proxy service) cho edge deployments. TypeORM có vấn đề connection management trong serverless.
7. Tình huống thực tế
Tình huống 1: SaaS Application (Multi-tenant)
Lựa chọn tốt nhất: Prisma
// Prisma xuất sắc với nested operations phức tạp
const tenant = await prisma.tenant.create({
data: {
name: 'Acme Corp',
subscription: { create: { plan: 'PRO', expiresAt: nextYear } },
users: {
create: [
{
email: '[email protected]',
role: 'ADMIN',
profile: { create: { bio: 'Tenant admin' } }
}
]
}
},
include: {
subscription: true,
users: { include: { profile: true } }
}
});Tại sao Prisma: Nested creates, type safety xuất sắc, Prisma Studio cho debugging, developer experience tuyệt vời cho phát triển tính năng nhanh.
Tình huống 2: Serverless API (Vercel, Cloudflare Workers)
Lựa chọn tốt nhất: Drizzle
// Drizzle: cold start tối thiểu, chạy trên edge
import { drizzle } from 'drizzle-orm/neon-http';
import { neon } from '@neondatabase/serverless';
const sql = neon(process.env.DATABASE_URL!);
const db = drizzle(sql);
export default async function handler(req: Request) {
const users = await db.select().from(usersTable).limit(10);
return Response.json(users);
}Tại sao Drizzle: ~50KB bundle, không binary engine, native edge support, hoạt động với Neon, PlanetScale, Turso serverless databases.
Tình huống 3: Enterprise Application (Complex Domain)
Lựa chọn tốt nhất: TypeORM
// TypeORM: patterns quen thuộc cho enterprise teams
@Entity()
@TableInheritance({ column: { type: 'varchar', name: 'type' } })
export abstract class Payment {
@PrimaryGeneratedColumn()
id: number;
@Column('decimal')
amount: number;
}
@ChildEntity()
export class CreditCardPayment extends Payment {
@Column()
cardLastFour: string;
}
@ChildEntity()
export class BankTransferPayment extends Payment {
@Column()
bankName: string;
}Tại sao TypeORM: Inheritance mapping, ActiveRecord cho rapid prototyping, patterns quen thuộc cho teams Java/C# chuyển sang TypeScript, hệ sinh thái trưởng thành.
Tình huống 4: Microservice với nhu cầu Performance cao
Lựa chọn tốt nhất: Drizzle
// Drizzle: queries tinh chỉnh tay với type safety
const result = await db
.select({
userId: users.id,
userName: users.name,
totalOrders: count(orders.id),
totalRevenue: sql<number>`SUM(${orders.amount})`,
})
.from(users)
.leftJoin(orders, eq(users.id, orders.userId))
.where(
and(
gt(orders.createdAt, sql`NOW() - INTERVAL '30 days'`),
eq(orders.status, 'completed')
)
)
.groupBy(users.id, users.name)
.orderBy(desc(sql`SUM(${orders.amount})`))
.limit(100);Tại sao Drizzle: Overhead gần bằng 0, SQL-level control, runtime footprint nhỏ nhất, tuyệt vời cho performance-critical services.
Lưu đồ quyết định
8. Migrate giữa các ORMs
Từ TypeORM sang Prisma
# 1. Introspect database hiện có
npx prisma db pull
# 2. Tự động tạo schema.prisma từ tables hiện có
# 3. Generate Prisma Client
npx prisma generate
# 4. Thay thế TypeORM queries dần dầnTrước (TypeORM):
const users = await userRepo.find({
where: { role: Role.ADMIN },
relations: { posts: true },
order: { createdAt: 'DESC' }
});Sau (Prisma):
const users = await prisma.user.findMany({
where: { role: 'ADMIN' },
include: { posts: true },
orderBy: { createdAt: 'desc' }
});Từ Prisma sang Drizzle
# 1. Dùng drizzle-kit để introspect database hiện có
npx drizzle-kit introspect
# 2. Tự động tạo schema.ts từ tables hiện có
# 3. Thay thế Prisma queries dần dầnTrước (Prisma):
const user = await prisma.user.findUnique({
where: { email: '[email protected]' },
include: { posts: true }
});Sau (Drizzle):
const user = await db.query.users.findFirst({
where: eq(users.email, '[email protected]'),
with: { posts: true }
});Tips Migration
- Đừng rewrite tất cả một lúc — migrate từng query một
- Giữ cả hai ORMs installed trong quá trình chuyển đổi
- Dùng database introspection để generate schema của ORM mới
- Viết integration tests trước khi migrate để bắt regressions
- Bắt đầu từ read queries (rủi ro thấp hơn), rồi migrate writes
9. Các lỗi thường gặp
Lỗi 1: N+1 Query Problem
// ❌ Sai — N+1 queries (tất cả ORMs)
const users = await getUsers();
for (const user of users) {
const posts = await getPostsByUserId(user.id); // N queries!
}
// ✅ Đúng — eager load relations
// Prisma
const users = await prisma.user.findMany({
include: { posts: true } // Một query duy nhất với JOIN
});
// TypeORM
const users = await userRepo.find({
relations: { posts: true }
});
// Drizzle
const users = await db.query.users.findMany({
with: { posts: true }
});Lỗi 2: TypeORM's synchronize trong Production
// ❌ KHÔNG BAO GIỜ làm điều này trong production
const dataSource = new DataSource({
synchronize: true // Có thể drop columns, tables, data!
});
// ✅ Luôn dùng migrations trong production
const dataSource = new DataSource({
synchronize: false,
migrations: ['./migrations/*.ts']
});Lỗi 3: Prisma Client chưa Regenerated
# Sau khi thay đổi schema.prisma, BẮT BUỘC phải regenerate
npx prisma generate
# Triệu chứng phổ biến: TypeScript types không khớp với schema
# Fix: chạy generate sau mỗi schema changeLỗi 4: Hai Query APIs của Drizzle
// Drizzle có HAI APIs — phục vụ mục đích khác nhau
// 1. SQL-like API (select/insert/update/delete)
// Tốt nhất cho: complex queries, aggregations, JOINs
const result = await db.select().from(users).where(eq(users.id, 1));
// 2. Relational Query API (query.table.findMany)
// Tốt nhất cho: load entities với relations
const result = await db.query.users.findFirst({
where: eq(users.id, 1),
with: { posts: true }
});
// Đừng nhầm lẫn — dùng đúng API cho đúng việcTóm tắt và Key Takeaways
✅ Prisma — developer experience tốt nhất, schema-first, auto-generated type-safe client, lý tưởng cho rapid development và SaaS apps
✅ TypeORM — decorator-based, quen thuộc với dev Java/C#, ActiveRecord + DataMapper patterns, tốt nhất cho enterprise teams
✅ Drizzle — SQL-like syntax, bundle nhỏ nhất (~50KB), tốt nhất cho serverless/edge và performance-critical apps
✅ Cả ba đều cung cấp full TypeScript type safety — lựa chọn phụ thuộc nhu cầu project
✅ Cho hầu hết projects mới, bắt đầu với Prisma (DX tốt nhất) hoặc Drizzle (performance tốt nhất)
✅ TypeORM tốt nhất khi team từ Java/C# và thích decorator patterns
✅ Cho serverless/edge, Drizzle là người thắng rõ ràng (bundle nhỏ nhất, không phụ thuộc engine)
✅ Migration giữa ORMs khả thi nhờ database introspection tools
✅ Tránh lỗi phổ biến: N+1 queries, TypeORM's synchronize, Prisma thiếu generate
Tiếp theo
Giờ bạn đã hiểu TypeScript ORMs, khám phá thêm:
Xây dựng với ORMs:
- TypeScript Backend Development - Xây APIs với TypeScript và Prisma
- Database Schema Design Guide - Thiết kế schema trước khi chọn ORM
Database cơ bản:
- Relational Database Fundamentals - Hiểu nền tảng
- SQL Fundamentals - Làm chủ SQL trước khi abstract nó
API Development:
- REST API là gì? - Hướng dẫn đầy đủ về REST principles
- GraphQL là gì? - Paradigm API thay thế (hoạt động tốt với cả ba ORMs)
So sánh liên quan:
- Bun vs Node.js vs Deno - Chọn runtime
- TypeScript Full-Stack Roadmap - Lộ trình học đầy đủ
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.