设计模式:Repository、Unit of Work 与架构分层
本教程共 54 篇 · 第 46 篇 · 更新于 2026-08-11 · 约 5 分钟阅读
本节目标:学会用 Repository、Unit of Work、Service 层组织 Prisma 代码,理解单例与工厂模式,掌握 NestJS 的 PrismaService 集成。
ORM 解决「怎么查」,设计模式解决「代码往哪放」。业务复杂后,查询散落在路由里,改一个表结构要翻遍全项目。分层是解药。
Repository:把查询细节关进抽屉
Repository 把数据访问封装成类。路由和 Service 只调方法,不碰 Prisma API:
import { PrismaClient, Prisma } from "./generated/prisma/client";
export class UserRepository {
constructor(private prisma: PrismaClient) {}
findById(id: string) {
return this.prisma.user.findUnique({ where: { id } });
}
findByEmail(email: string) {
return this.prisma.user.findUnique({ where: { email } });
}
create(data: Prisma.UserCreateInput) {
return this.prisma.user.create({ data });
}
}
好处两条:一是封装,软删除过滤、字段脱敏写进仓库,业务代码保持干净;二是可测试,单测 mock 仓库类即可,不用 mock 整个 PrismaClient。
NoteRepository 别过度设计。接口套接口、基类套基类,只会增加理解成本。团队约定「Service 不直接碰 Prisma」就够用,等真的需要换数据库或写单元测试时再加深抽象。
Unit of Work:事务边界就是业务边界
多个写入要一起成功或一起失败,用事务包住。Unit of Work 模式的核心:把事务客户端 tx 注入仓库,让所有仓库共用同一事务:
await prisma.$transaction(async (tx) => {
const users = new UserRepository(tx);
const audits = new AuditRepository(tx);
await users.create(userData);
await audits.log("user.create");
});
tx 与 PrismaClient 类型兼容,直接传入即可。事务边界划在调用方(Service),仓库不感知事务,职责单一。单步批量写入(createMany、updateMany、deleteMany)本身自动是事务,不需要套交互式事务;交互式事务留给「多步操作必须同生共死」的场景。
Tip交互式事务默认 5 秒超时,事务内别调外部 API。事务细节见第 26、27 章。
Service 层:编排与校验
Service 层放业务规则:入参校验、权限判断、事务边界、仓库编排:
export class UserService {
constructor(private prisma: PrismaClient) {}
async register(input: unknown) {
const data = createUserSchema.parse(input); // 运行时校验
return this.prisma.$transaction(async (tx) => {
const user = await new UserRepository(tx).create(data);
await new AuditRepository(tx).log("user.register", user.id);
return user;
});
}
}
控制器只管 HTTP,Service 只管业务,Repository 只管数据。三层各司其职,改动互不牵连。
单例与工厂
PrismaClient 每个进程只该有一个实例。每请求 new 一个,就是每请求开一个连接池,数据库很快被拖垮。单例模式:
import { PrismaClient } from "./generated/prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";
const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }),
});
if (process.env.NODE_ENV !== "production") {
globalForPrisma.prisma = prisma;
}
挂在 globalThis 上,Next.js 热更新不会反复重建客户端。生产环境不用挂,进程内自然只有一个。单例与连接池原理见第 17 章。
工厂模式按需生产客户端。多租户 DB-per-tenant 场景,每个租户一个客户端:
export function createTenantClient(databaseUrl: string) {
return new PrismaClient({
adapter: new PrismaPg({ connectionString: databaseUrl }),
});
}
工厂也常用于测试:批量构造测试数据、生成不同环境的客户端。
NestJS:PrismaService 集成
NestJS 的依赖注入和 Prisma 配合很好。官方推荐把 PrismaClient 包成可注入的 PrismaService(生命周期钩子管理连接、@Global 模块注册一次),完整集成写法见第 47 章,这里不重复。
CQRS 与 DDD:知道边界在哪
最后两个概念,生产项目常被提起。CQRS:读和写分开——写走命令(校验 + 事务),读走查询(优化投影,可缓存)。DDD:聚合根维护业务不变量,值对象是不可变类型,限界上下文对应独立的 Prisma Schema。
Note小项目不要硬上 CQRS/DDD。它们的价值在复杂业务域;CRUD 为主的应用,Service + Repository 已经足够。真要上,也先从「读模型单独建表」这种低成本改造开始,别一上来就推倒重来。
参考来源
- Mapagam:Implementing Design Patterns
- Generalist Programmer:NestJS Prisma Tutorial(原文 v6 写法,已按 v7 改写)
- Prisma 官方文档:Best practices(连接池与单例)