Back to blog

ORM Guide: Prisma vs TypeORM vs Drizzle

ormtypescriptdatabasenodejsbackend
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


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ậnType SafetyLearning CurveSQL ControlProductivity
Raw SQL (pg, mysql2)❌ KhôngThấpToàn bộThấp
Query Builder (Knex.js)⚠️ Một phầnTrung bìnhCaoTrung bình
Drizzle ORM✅ Đầy đủThấp-TBCaoCao
TypeORM✅ Đầy đủTB-CaoTrung bìnhCao
Prisma✅ Đầy đủThấpThấp-TBRấ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 init

Tạ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ấtKhông viết raw SQL type-safe được
API trực quan — dễ họcPrisma Client thêm bundle size
Prisma Studio (visual DB browser)Schema language là proprietary
Quản lý migration xuất sắcComplex raw queries cần $queryRaw
Types auto-generated, luôn in-syncJoins thực hiện ở application level
Documentation tuyệt vờiMộ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 patternsType safety gaps (relations có thể undefined)
QueryBuilder mạnh mẽsynchronize: true có thể phá hủy data production
Hỗ trợ nhiều databasesRelease cycle chậm, nhiều issues mở
Hệ sinh thái trưởng thànhEntity metadata cần reflect-metadata
Transaction support xuất sắcSetup 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 leakRelations 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 generationM: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ầnRelational 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ăngPrismaTypeORMDrizzle
Ngôn ngữ.prisma DSLTypeScript decoratorsTypeScript functions
Vị tríschema.prismaEntity classesSchema files
RelationsImplicit qua fieldsDecorators (@OneToMany)Explicit relations()
Enumsenum trong schemaTypeScript enum + decoratorpgEnum()
Indexes@@index@Index()Table callback
ValidationSchema-levelDecorator-basedSchema-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ăngPrismaTypeORMDrizzle
Type Safety⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
Learning CurveDễTB-KhóDễ-TB
Bundle Size~10MB (engine)~2MB~50KB
Raw SQL Support$queryRawquery()sql template
Migration ToolBuilt-inBuilt-inDrizzle Kit
Visual StudioPrisma Studio—Drizzle Studio
DatabasesPG, MySQL, SQLite, MongoDB, CockroachDB, SQL ServerPG, MySQL, SQLite, SQL Server, Oracle, MongoDBPG, MySQL, SQLite, Turso
Code GenerationBắt buộc (prisma generate)Không cầnKhô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 SupportNode.js (Bun một phần)Node.jsNode, Bun, Deno

So sánh Migration

Khía cạnhPrismaTypeORMDrizzle
GenerationTừ schema diffTừ entity diffTừ schema diff
FormatSQL filesTypeScript classesSQL files
Applyprisma migrate deploymigration:rundrizzle-kit migrate
RevertThủ côngmigration:revertThủ công
Dev modeprisma migrate devsynchronize: true (nguy hiểm!)drizzle-kit push
Seedprisma db seedScript tùy chỉnhScript 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:

MetricPrismaTypeORMDrizzle
Cold StartChậm (engine init)Trung bìnhNhanh
Query OverheadCao hơn (Rust engine IPC)Trung bình (JS query building)Tối thiểu (thin layer)
Connection PoolingBuilt-inQua driverQua driver
ServerlessCần Prisma AccelerateVấn đề cold startTốt nhất (bundle nhỏ nhất)
Chống N+1include giải quyếtrelations thủ côngwith trong query API
Batch OperationscreateMany, updateManysave(array), QueryBuilderinsert().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ần

Trướ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ần

Trướ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

  1. Đừng rewrite tất cả một lúc — migrate từng query một
  2. Giữ cả hai ORMs installed trong quá trình chuyển đổi
  3. Dùng database introspection để generate schema của ORM mới
  4. Viết integration tests trước khi migrate để bắt regressions
  5. 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 change

Lỗ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ệc

Tó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:

Database cơ bản:

API Development:

So sánh liên quan:


📬 Đă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.