Skip to main content

Lesson 3: Nâng cao & Vận hành

📋 Agenda

Thời gian đọc ước tính: ~18 phút

Sau bài này, bạn sẽ:

  • Mở rộng Prisma Client với Extensions để thêm computed fields và custom logic
  • Cấu hình Logging để debug query hiệu quả trong từng môi trường
  • Mock Prisma Client đúng cách để viết Unit Test không phụ thuộc DB
  • Hiểu vấn đề Connection Pooling trong Serverless và cách giải quyết

Yêu cầu đầu vào (Prerequisites):

  • 🔹 Đã hoàn thành Lesson 2 (hiểu Prisma Client API)
  • 🔹 Biết cơ bản về Unit Testing (Jest hoặc Vitest)

❓ Vấn đề & Giải pháp

Vấn đề (Problem Statement):

  • Cần thêm business logic (computed fields, soft-delete logic) vào tất cả query mà không muốn lặp code
  • Khó debug khi query chậm — không biết Prisma đang sinh ra SQL gì
  • Unit test phụ thuộc database thật → test chậm và không ổn định
  • Serverless (Lambda, Vercel) tạo connection mới mỗi request → database bị quá tải connection

Giải pháp (Solution):

  • Client Extensions — Middleware cho Prisma Client, thêm logic tập trung
  • Logging config — Prisma logs query/error ra console hoặc custom logger
  • Jest/Vitest mock — Mock toàn bộ Prisma Client mà không cần DB
  • Prisma Accelerate / PgBouncer — Connection pooler xử lý bài toán serverless

📖 Prisma Client Extensions

Định nghĩa: Extensions là một cơ chế cho phép mở rộng hoặc thay đổi behavior của Prisma Client, bao gồm: thêm computed fields, custom methods, hoặc intercept query.

Thêm Computed Fields

Computed fields là các field được tính toán tại runtime, không lưu trong DB.

// filename: src/lib/prisma.extended.ts

import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

// $extends() tạo ra một Extended Client mới
// Không mutate prisma gốc — đảm bảo immutability
export const extendedPrisma = prisma.$extends({
result: {
// Mở rộng model 'user'
user: {
// Computed field: fullName tự động ghép từ firstName và lastName
fullName: {
// Khai báo field cần thiết để tính toán
needs: { firstName: true, lastName: true },
compute(user) {
return `${user.firstName} ${user.lastName}`;
},
},
// Computed field: kiểm tra xem user có phải admin không
isAdmin: {
needs: { role: true },
compute(user) {
return user.role === 'ADMIN';
},
},
},
},
});

// Sử dụng:
const user = await extendedPrisma.user.findUnique({ where: { id: 1 } });
console.log(user?.fullName); // "Nguyen Van A" — TypeScript biết field này tồn tại
console.log(user?.isAdmin); // true/false

Custom Model Methods

// filename: src/lib/prisma.extended.ts (tiếp theo)

export const extendedPrisma = prisma.$extends({
model: {
// Thêm custom method cho tất cả model
$allModels: {
// findOrCreate: Tìm hoặc tạo mới — pattern hay dùng
async findOrCreate<T>(
this: T,
args: { where: Record<string, unknown>; create: Record<string, unknown> }
) {
const ctx = Prisma.getExtensionContext(this) as any;
return (
(await ctx.findFirst({ where: args.where })) ??
(await ctx.create({ data: args.create }))
);
},
},
// Hoặc chỉ áp dụng cho model cụ thể
post: {
async publishAll(authorId: number) {
return prisma.post.updateMany({
where: { authorId, published: false },
data: { published: true },
});
},
},
},
});

// Sử dụng:
await extendedPrisma.post.publishAll(1);

Query Extensions — Middleware-style

// Tự động thêm soft-delete filter cho mọi query
export const softDeletePrisma = prisma.$extends({
query: {
$allModels: {
async findMany({ model, operation, args, query }) {
// Tự động thêm deletedAt: null vào WHERE nếu không có
if (!args.where?.deletedAt) {
args.where = { ...args.where, deletedAt: null };
}
return query(args);
},
},
},
});

⚠️ Lưu ý thực tế: Extensions thêm overhead nhỏ. Với hệ thống cực kỳ cao tải, hãy benchmark trước. Đối với hầu hết dự án, overhead này không đáng kể.


🔨 Observability — Logging & Debugging

Cấu hình Logging

// filename: src/lib/prisma.ts

import { PrismaClient } from '@prisma/client';

// Cấu hình log level theo môi trường
const prisma = new PrismaClient({
log:
process.env.NODE_ENV === 'development'
? [
{ level: 'query', emit: 'event' }, // Emit event thay vì console.log
{ level: 'error', emit: 'stdout' },
{ level: 'warn', emit: 'stdout' },
]
: [{ level: 'error', emit: 'stdout' }], // Production: chỉ log lỗi
});

// Bắt event và xử lý tùy chỉnh — tích hợp với logger (Winston, Pino...)
prisma.$on('query', (event) => {
console.log({
query: event.query, // Câu SQL đã generate
params: event.params, // Các parameter được bind
duration: event.duration, // Thời gian thực thi (ms)
timestamp: event.timestamp,
});

// Cảnh báo nếu query chậm (> 1s)
if (event.duration > 1000) {
console.warn(`⚠️ Slow query detected: ${event.duration}ms`);
}
});

Debug Query nhanh

// Xem SQL Prisma tạo ra KHÔNG cần chạy thực sự
const sql = await prisma.user.findMany({
where: { isActive: true },
})
// Hoặc dùng --log=query Environment variable:
// DATABASE_URL=... LOG_LEVEL=query ts-node src/main.ts

// Sử dụng explain() (PostgreSQL) để xem query plan
const queryPlan = await prisma.$queryRaw`
EXPLAIN ANALYZE
SELECT * FROM "User" WHERE "isActive" = true
`;
console.log(queryPlan);

🔨 Testing — Mock Prisma Client

Chiến lược Mock

Cài đặt với Vitest / Jest

npm install -D vitest @vitest/coverage-v8
# Hoặc: npm install -D jest @types/jest ts-jest

Manual Mock với DeepMock pattern

// filename: src/__mocks__/prisma.ts

import { PrismaClient } from '@prisma/client';
import { mockDeep, mockReset, DeepMockProxy } from 'vitest-mock-extended';
// npm install -D vitest-mock-extended

// Tạo deep mock của toàn bộ PrismaClient
export const prismaMock = mockDeep<PrismaClient>();
// filename: src/__tests__/user.service.test.ts

import { vi, it, expect, beforeEach, describe } from 'vitest';
import { prismaMock } from '../__mocks__/prisma';
import { UserService } from '../services/user.service';

// Mock module để UserService nhận prismaMock thay vì real client
vi.mock('../lib/prisma', () => ({ prisma: prismaMock }));

describe('UserService', () => {
beforeEach(() => {
// Reset giữa các test để tránh state leak
mockReset(prismaMock);
});

it('should return user by id', async () => {
// arrange: Định nghĩa mock return value
const mockUser = { id: 1, email: 'test@example.com', name: 'Test User' };
prismaMock.user.findUnique.mockResolvedValue(mockUser as any);

// act
const service = new UserService();
const user = await service.findById(1);

// assert
expect(user).toEqual(mockUser);
expect(prismaMock.user.findUnique).toHaveBeenCalledWith({
where: { id: 1 },
});
});

it('should return null when user not found', async () => {
prismaMock.user.findUnique.mockResolvedValue(null);

const service = new UserService();
const user = await service.findById(999);

expect(user).toBeNull();
});
});

💡 Pro-tip: Nếu không muốn thêm dependency vitest-mock-extended, có thể tạo manual mock đơn giản hơn bằng vi.fn() cho từng method cần mock. Tuy nhiên vitest-mock-extended tự động type-safe toàn bộ.


🔨 Deployment — Connection Pooling trong Serverless

Vấn đề Connection Pool

Hậu quả: Với 10+ function instances, mỗi instance mở 10 connections → 100+ connections → PostgreSQL bị quá tải → Lỗi "Too many connections".

Giải pháp 1: PgBouncer (Self-managed)

# filename: .env — Thêm ?pgbouncer=true vào connection string
DATABASE_URL="postgresql://user:pass@pgbouncer-host:6432/mydb?pgbouncer=true&connection_limit=1"
// Với PgBouncer, mỗi Prisma Client chỉ cần 1 connection
const prisma = new PrismaClient({
datasources: {
db: {
url: process.env.DATABASE_URL, // URL đã cấu hình PgBouncer
},
},
});

Prisma Accelerate là connection pooler managed của Prisma — không cần cấu hình server, tích hợp qua API key.

npm install @prisma/extension-accelerate
// filename: src/lib/prisma.ts

import { PrismaClient } from '@prisma/client';
import { withAccelerate } from '@prisma/extension-accelerate';

const prisma = new PrismaClient().$extends(withAccelerate());

// Sử dụng cache tại query level (feature bổ sung của Accelerate)
const users = await prisma.user.findMany({
cacheStrategy: {
ttl: 60, // Cache 60 giây — giảm số query đến DB
swr: 10, // Stale-while-revalidate: trả cache cũ, background refresh
},
});
# filename: .env
# Thay DATABASE_URL thường bằng Accelerate connection string
DATABASE_URL="prisma://accelerate.prisma-data.net/?api_key=YOUR_KEY"
Giải phápChi phíĐộ phức tạpPhù hợp
Singleton PatternMiễn phíThấpLong-running server (Express/NestJS)
PgBouncerServer costTrung bìnhSelf-hosted production
Prisma AcceleratePay-per-requestThấpServerless (Vercel, Lambda)

🚀 Trade-off & Pitfalls

✅ NÊN❌ KHÔNG nên
Mock Prisma Client trong Unit TestKết nối DB thật trong Unit Test
Log query event để tìm slow queryconsole.log từng câu SQL thủ công
Dùng Accelerate/PgBouncer ở Serverlessnew PrismaClient() không kiểm soát trong Lambda
mockReset() giữa các test caseĐể state mock leak giữa các test

⚠️ Pitfalls hay gặp

1. Extensions không chain được tùy tiện Khi dùng nhiều $extends(), kết quả cần được assign lại vào một biến mới. Đừng gán vào prisma gốc.

2. Slow query không phải lúc nào cũng do Prisma Trước khi tối ưu Prisma query, hãy kiểm tra index trong database. Một query đơn giản thiếu index có thể chậm hơn query phức tạp có index.

3. Testing với Transaction Nếu service dùng prisma.$transaction(), mock cần xử lý callback transaction. Cân nhắc dùng integration test với database test riêng thay vì mock phức tạp.


🗺️ MECE Mindmap


Made by Anh Tu - Share to be share