Lesson 2: Prisma Client — Truy vấn & Type Safety
📋 Agenda
Thời gian đọc ước tính: ~25 phút
Sau bài này, bạn sẽ:
- ✅ Khởi tạo và quản lý
PrismaClientinstance đúng cách - ✅ Thực hiện đầy đủ thao tác CRUD với Prisma Client API
- ✅ Sử dụng
include,selectvà Nested Writes để thao tác với dữ liệu quan hệ - ✅ Xử lý kiểu đặc biệt:
JSON,Enum,DateTime - ✅ Viết Raw SQL an toàn với
$queryRaw
Yêu cầu đầu vào (Prerequisites):
- 🔹 Đã hoàn thành Lesson 1 (hiểu cấu trúc
schema.prisma) - 🔹 Biết async/await và TypeScript generics cơ bản
❓ Vấn đề & Giải pháp
Vấn đề (Problem Statement):
- ORM thiếu type-safe: query trả về
any→ lỗi undefined field chỉ phát hiện khi runtime - Không có autocomplete khi viết query → developer phải nhớ tên field/relation
- Nested operations (tạo parent + child cùng lúc) thường phức tạp, dễ quên transaction
Giải pháp (Solution):
Prisma Client được auto-generated từ schema.prisma, đảm bảo:
- Mọi method, field, và return type đều có TypeScript type chính xác 100%
- IDE (VS Code) sẽ autocomplete đầy đủ field name, relation, và filter options
- Nested Writes cho phép thao tác parent-child trong một lần gọi duy nhất
📖 Prisma Client là gì?
Định nghĩa: Prisma Client là một thư viện query builder type-safe được sinh tự động t ừ schema.prisma. Mỗi lần schema thay đổi, chạy prisma generate để cập nhật.
🔨 Setup và Khởi tạo PrismaClient
Cài đặt
npm install @prisma/client
npx prisma generate # Sinh Prisma Client từ schema
Singleton Pattern — Tránh quá nhiều kết nối
// filename: src/lib/prisma.ts
import { PrismaClient } from '@prisma/client';
// Singleton pattern: đảm bảo chỉ có 1 instance trong toàn app
// Đặc biệt quan trọng với Next.js (Hot Reload tạo nhiều instance)
const globalForPrisma = global as unknown as { prisma: PrismaClient };
export const prisma =
globalForPrisma.prisma ||
new PrismaClient({
log: ['query', 'error', 'warn'], // Log mọi thao tác trong dev
});
if (process.env.NODE_ENV !== 'production') {
// Chỉ lưu vào global trong dev để tránh hot-reload tạo nhiều instance
globalForPrisma.prisma = prisma;
}
// filename: src/app.ts — Sử dụng trong NestJS/Express
import { prisma } from './lib/prisma';
// Đảm bảo disconnect khi app shutdown
async function main() {
try {
await app.listen(3000);
} finally {
// Giải phóng connection pool khi tắt server
await prisma.$disconnect();
}
}
🔨 CRUD Cơ bản
findUnique — Tìm theo khóa duy nhất
// filename: src/services/user.service.ts
import { prisma } from '../lib/prisma';
// findUnique: CHỈ query bằng @id hoặc @unique field
// Trả về: User | null
const user = await prisma.user.findUnique({
where: { id: 1 },
});
// Với composite unique key
const post = await prisma.post.findUnique({
where: {
authorId_slug: { authorId: 1, slug: 'my-post' }, // Composite unique
},
});
findFirst & findMany — Tìm một hoặc nhiều
// findFirst: Lấy bản ghi đầu tiên khớp với điều kiện
// Trả về: User | null
const firstAdmin = await prisma.user.findFirst({
where: { role: 'ADMIN' },
orderBy: { createdAt: 'desc' }, // Lấy admin mới nhất
});
// findMany: Lấy nhiều bản ghi
// Trả về: User[]
const allUsers = await prisma.user.findMany({
where: { isActive: true },
orderBy: [
{ createdAt: 'desc' }, // Sort chính: mới nhất
{ name: 'asc' }, // Sort phụ: theo tên
],
take: 10, // Limit
skip: 20, // Offset (trang 3 nếu take=10)
});
create, update, delete
// --- CREATE ---
const newUser = await prisma.user.create({
data: {
email: 'tu@example.com',
name: 'Anh Tu',
},
// select: Chỉ trả về các field cần thiết (tối ưu performance)
select: {
id: true,
email: true,
createdAt: true,
},
});
// --- UPDATE ---
const updatedUser = await prisma.user.update({
where: { id: 1 },
data: {
name: 'Anh Tu Updated',
// Prisma có atomic operators: increment, decrement, multiply...
// loginCount: { increment: 1 },
},
});
// --- UPSERT (Create or Update) ---
const upserted = await prisma.user.upsert({
where: { email: 'tu@example.com' },
update: { name: 'Tu Updated' },
create: { email: 'tu@example.com', name: 'Tu New' },
});
// --- DELETE ---
// Xóa vĩnh viễn. Dùng soft-delete thay thế trong production
const deleted = await prisma.user.delete({
where: { id: 1 },
});
// --- deleteMany (Bulk delete) ---
const { count } = await prisma.post.deleteMany({
where: { published: false, createdAt: { lt: new Date('2024-01-01') } },
});
console.log(`Đã xóa ${count} bài viết nháp cũ`);
🔨 Relations — Thao tác dữ liệu quan hệ
include — Eager Loading
// filename: src/services/post.service.ts
// include: Lấy kèm dữ liệu từ bảng liên quan (tương đương JOIN)
const userWithPosts = await prisma.user.findUnique({
where: { id: 1 },
include: {
posts: {
where: { published: true }, // Filter lồng nhau
orderBy: { createdAt: 'desc' },
take: 5, // Lấy 5 bài viết mới nhất
},
},
});
// userWithPosts.posts → Post[] (TypeScript biết rõ kiểu này)
select — Chỉ lấy field cần thiết
// select: Kiểm soát chính xác field nào được trả về
// Dùng khi API response không cần toàn bộ data → giảm bandwidth
const users = await prisma.user.findMany({
select: {
id: true,
name: true,
email: true,
// Lấy kèm relation với select lồng nhau
posts: {
select: {
id: true,
title: true,
// Không lấy content (có thể rất lớn)
},
},
// _count: Đếm số lượng relation mà không cần tải dữ liệu
_count: {
select: { posts: true },
},
},
});
// users[0]._count.posts → number