repository-pattern-nestjs
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseRepository Pattern - Prisma/NestJS
仓库模式 - Prisma/NestJS
Complete implementation guide for NestJS Repositories using Prisma.
使用Prisma实现NestJS仓库的完整指南。
Canonical Examples
标准示例
Study these real implementations as the source of truth:
- Model Repository: task-template.repository.ts
- Base Repository: base.repository.ts
Detailed code examples: See references/repository-examples.md
请将这些真实实现作为权威参考:
- 模型仓库:task-template.repository.ts
- 基础仓库:base.repository.ts
详细代码示例:查看references/repository-examples.md
Core Responsibilities
核心职责
Repositories act as data access abstraction. They should:
- Encapsulate queries - All database operations go through repositories
- Hide database details - Services don't know SQL or ORM specifics
- Provide typed interfaces - Strong typing for queries and results
- Handle errors - Low-level errors are handled here
- Implement soft deletes - Consistent deletion strategy
- Support common patterns - Find, create, update, delete operations
- Optimize queries - Eager loading, indexes, pagination
仓库作为数据访问的抽象层,应具备以下能力:
- 封装查询 - 所有数据库操作都通过仓库执行
- 隐藏数据库细节 - 服务层无需了解SQL或ORM的具体实现
- 提供类型化接口 - 为查询和结果提供强类型支持
- 处理错误 - 在这一层处理底层错误
- 实现软删除 - 保持一致的删除策略
- 支持通用操作 - 实现查询、创建、更新、删除等常见操作
- 优化查询 - 预加载、索引、分页等优化手段
BaseRepository Extension
继承BaseRepository
🔴 Critical: All repositories MUST extend .
BaseRepository<T, C, U, W>[!IMPORTANT] Why Extend BaseRepository?
- Standardization: Consistent CRUD interface across all repositories
- Soft Delete: Automatic
filtering in base methodsdeletedAt: null- Type Safety: Generic types ensure compile-time correctness
- Reusability: Inherit common methods (create, findOne, update, etc.)
- Maintainability: Bug fixes in BaseRepository benefit all repositories
[!NOTE] ModelWrapper — Current Implementation (Planned for Simplification) All existing repositories use aclass that bridgesModelWrappergenerics to Prisma delegates. This wrapper adds no business value and will be simplified in a future phase (Phase 4) to inject PrismaService directly. For now, follow this pattern to stay consistent with the existing codebase.BaseRepository
typescript
import { BaseRepository, IBaseModel } from '@/lib/repositories/base.repository';
// 1. Define Wrapper (bridges BaseRepo generics to Prisma Delegate)
class UserModelWrapper implements IBaseModel<User, Prisma.UserCreateInput, Prisma.UserUpdateInput, Prisma.UserWhereInput> {
constructor(private readonly delegate: Prisma.UserDelegate) {}
create(args: any) { return this.delegate.create(args); }
findFirst(args: any) { return this.delegate.findFirst(args); }
findMany(args: any) { return this.delegate.findMany(args); }
update(args: any) { return this.delegate.update(args); }
delete(args: any) { return this.delegate.delete(args); }
count(args: any) { return this.delegate.count(args); }
}
// 2. Implement Repository
@Injectable()
export class UserRepository extends BaseRepository<
User,
Prisma.UserCreateInput,
Prisma.UserUpdateInput,
Prisma.UserWhereInput
> {
constructor(private readonly prisma: PrismaService) {
super(new UserModelWrapper(prisma.user));
}
}🔴 关键要求:所有仓库必须继承。
BaseRepository<T, C, U, W>[!IMPORTANT] 为什么要继承BaseRepository?
- 标准化:所有仓库拥有一致的CRUD接口
- 软删除支持:基础方法中自动过滤
的记录deletedAt: null- 类型安全:泛型确保编译时的正确性
- 可复用性:继承通用方法(创建、查询单个、更新等)
- 可维护性:BaseRepository中的bug修复会惠及所有仓库
[!NOTE] ModelWrapper — 当前实现(计划简化) 所有现有仓库都使用类,用于连接ModelWrapper泛型与Prisma委托。 该包装类不具备业务价值,将在后续阶段(第4阶段)简化为直接注入PrismaService。 目前请遵循此模式,以保持与现有代码库的一致性。BaseRepository
typescript
import { BaseRepository, IBaseModel } from '@/lib/repositories/base.repository';
// 1. 定义Wrapper(连接BaseRepo泛型与Prisma委托)
class UserModelWrapper implements IBaseModel<User, Prisma.UserCreateInput, Prisma.UserUpdateInput, Prisma.UserWhereInput> {
constructor(private readonly delegate: Prisma.UserDelegate) {}
create(args: any) { return this.delegate.create(args); }
findFirst(args: any) { return this.delegate.findFirst(args); }
findMany(args: any) { return this.delegate.findMany(args); }
update(args: any) { return this.delegate.update(args); }
delete(args: any) { return this.delegate.delete(args); }
count(args: any) { return this.delegate.count(args); }
}
// 2. 实现仓库
@Injectable()
export class UserRepository extends BaseRepository<
User,
Prisma.UserCreateInput,
Prisma.UserUpdateInput,
Prisma.UserWhereInput
> {
constructor(private readonly prisma: PrismaService) {
super(new UserModelWrapper(prisma.user));
}
}Specialized Find Methods
专用查询方法
🟡 Recommended: Implement domain-specific queries here (not in Service).
typescript
// Find by UID — use findOne({ uid, deletedAt: null }) from BaseRepository
// instead of adding a redundant findByUid wrapper per repository.
// Only add findByUid if it has additional logic (e.g., includes, scoping).
async findByUid(uid: string, include?: Prisma.UserInclude): Promise<User | null> {
return this.model.findFirst({
where: { uid, deletedAt: null },
...(include && { include }),
});
}
// Find with Relations (Type-safe)
async findByUidWithProfile(uid: string): Promise<User & { profile: Profile } | null> {
return this.model.findFirst({
where: { uid, deletedAt: null },
include: { profile: true },
});
}[!WARNING] Do NOT implementin repositories. The Controller-Checks Pattern requires services to returnfindByUidOrThrowand controllers to callnull. Throwing in the repository bypasses this pattern and couples the data layer to HTTP semantics. UseensureResourceExists()and let the controller handle the 404.findOne({ uid, deletedAt: null })
🟡 推荐实践:在此处实现领域特定的查询逻辑(而非在服务层)。
typescript
// 根据UID查询 — 使用BaseRepository中的findOne({ uid, deletedAt: null })
// 无需为每个仓库添加冗余的findByUid包装方法。
// 仅当需要额外逻辑(如关联查询、作用域)时,才添加findByUid方法。
async findByUid(uid: string, include?: Prisma.UserInclude): Promise<User | null> {
return this.model.findFirst({
where: { uid, deletedAt: null },
...(include && { include }),
});
}
// 带关联的查询(类型安全)
async findByUidWithProfile(uid: string): Promise<User & { profile: Profile } | null> {
return this.model.findFirst({
where: { uid, deletedAt: null },
include: { profile: true },
});
}[!WARNING] 请勿在仓库中实现控制器校验模式要求服务层返回findByUidOrThrow,由控制器调用null。 在仓库中抛出异常会绕过此模式,并将数据层与HTTP语义耦合。 请使用ensureResourceExists(),并由控制器处理404错误。findOne({ uid, deletedAt: null })
Join/Association Table Repositories
关联表仓库
Join table repositories follow the same BaseRepository pattern.
The repository is always private to its module (never exported).
The module's service wraps the repository and provides the public API.
Example — TaskTargetRepository is a join table with no extra fields:
typescript
// The repo exists as a file but is only referenced inside the module
@Injectable()
export class TaskTargetRepository extends BaseRepository<TaskTarget, ...> {
async findByShowId(showId: bigint): Promise<TaskTarget[]> { ... }
async findByTaskId(taskId: bigint): Promise<TaskTarget[]> { ... }
}
// The service is what gets exported
@Injectable()
export class TaskTargetService extends BaseModelService {
async create(...args) { return this.repo.create(...args); }
async findByShowId(showId: bigint) { return this.repo.findByShowId(showId); }
}关联表仓库遵循相同的BaseRepository模式。
该仓库始终为模块私有(绝不导出)。
模块的服务层会封装该仓库,并提供公开API。
示例 — TaskTargetRepository是无额外字段的关联表:
typescript
// 仓库文件存在,但仅在模块内部引用
@Injectable()
export class TaskTargetRepository extends BaseRepository<TaskTarget, ...> {
async findByShowId(showId: bigint): Promise<TaskTarget[]> { ... }
async findByTaskId(taskId: bigint): Promise<TaskTarget[]> { ... }
}
// 服务层才是对外导出的部分
@Injectable()
export class TaskTargetService extends BaseModelService {
async create(...args) { return this.repo.create(...args); }
async findByShowId(showId: bigint) { return this.repo.findByShowId(showId); }
}Advanced Filtering with Pagination
带分页的高级过滤
🔴 Critical: Repositories should accept domain-level parameters and build Prisma where clauses internally.
typescript
async findPaginated(params: {
skip?: number;
take?: number;
name?: string;
uid?: string;
includeDeleted?: boolean;
studioUid?: string;
orderBy?: 'asc' | 'desc';
}): Promise<{ data: TaskTemplate[]; total: number }> {
const { skip, take, name, uid, includeDeleted, studioUid, orderBy } = params;
// Repository builds Prisma where clause
const where: Prisma.TaskTemplateWhereInput = {};
if (!includeDeleted) {
where.deletedAt = null;
}
if (name) {
where.name = { contains: name, mode: 'insensitive' };
}
if (uid) {
where.uid = { contains: uid, mode: 'insensitive' };
}
if (studioUid) {
where.studio = { uid: studioUid };
}
const [data, total] = await Promise.all([
this.model.findMany({
skip,
take,
where,
orderBy: orderBy ? { createdAt: orderBy } : undefined,
}),
this.model.count({ where }),
]);
return { data, total };
}Why This Pattern?
- Service layer stays ORM-agnostic (no Prisma types)
- Repository encapsulates all filter-building logic
- Easy to add new filters without changing service
- Testable without mocking Prisma types
🔴 关键要求:仓库应接收领域级参数,并在内部构建Prisma查询条件。
typescript
async findPaginated(params: {
skip?: number;
take?: number;
name?: string;
uid?: string;
includeDeleted?: boolean;
studioUid?: string;
orderBy?: 'asc' | 'desc';
}): Promise<{ data: TaskTemplate[]; total: number }> {
const { skip, take, name, uid, includeDeleted, studioUid, orderBy } = params;
// 仓库内部构建Prisma查询条件
const where: Prisma.TaskTemplateWhereInput = {};
if (!includeDeleted) {
where.deletedAt = null;
}
if (name) {
where.name = { contains: name, mode: 'insensitive' };
}
if (uid) {
where.uid = { contains: uid, mode: 'insensitive' };
}
if (studioUid) {
where.studio = { uid: studioUid };
}
const [data, total] = await Promise.all([
this.model.findMany({
skip,
take,
where,
orderBy: orderBy ? { createdAt: orderBy } : undefined,
}),
this.model.count({ where }),
]);
return { data, total };
}此模式的优势:
- 服务层与ORM解耦(无需使用Prisma类型)
- 仓库封装了所有过滤条件的构建逻辑
- 无需修改服务层即可轻松添加新的过滤条件
- 无需模拟Prisma类型即可进行测试
Optimistic Locking
乐观锁
🟡 Recommended: Implement for versioned entities.
updateWithVersionCheck()typescript
import { VersionConflictError } from '@/lib/errors/version-conflict.error';
import { PRISMA_ERROR } from '@/lib/errors/prisma-error-codes';
async updateWithVersionCheck(
where: Prisma.TaskTemplateWhereUniqueInput & { version?: number },
data: Prisma.TaskTemplateUpdateInput,
include?: Prisma.TaskTemplateInclude,
): Promise<TaskTemplate> {
try {
return await this.prisma.taskTemplate.update({
where: { ...where, deletedAt: null },
data,
...(include && { include }),
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === PRISMA_ERROR.RecordNotFound && where.version) {
const existing = await this.findOne({ uid: where.uid, deletedAt: null });
if (!existing) {
throw error; // Actually not found
}
throw new VersionConflictError(
'Task template version is outdated',
where.version,
existing.version,
);
}
}
throw error;
}
}🟡 推荐实践:为带版本的实体实现方法。
updateWithVersionCheck()typescript
import { VersionConflictError } from '@/lib/errors/version-conflict.error';
import { PRISMA_ERROR } from '@/lib/errors/prisma-error-codes';
async updateWithVersionCheck(
where: Prisma.TaskTemplateWhereUniqueInput & { version?: number },
data: Prisma.TaskTemplateUpdateInput,
include?: Prisma.TaskTemplateInclude,
): Promise<TaskTemplate> {
try {
return await this.prisma.taskTemplate.update({
where: { ...where, deletedAt: null },
data,
...(include && { include }),
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === PRISMA_ERROR.RecordNotFound && where.version) {
const existing = await this.findOne({ uid: where.uid, deletedAt: null });
if (!existing) {
throw error; // 实际未找到记录
}
throw new VersionConflictError(
'Task template version is outdated',
where.version,
existing.version,
);
}
}
throw error;
}
}Best Practices Checklist
最佳实践检查清单
- 🔴 Critical: Extend (never implement repositories from scratch)
BaseRepository - Create proper ModelWrapper implementing (current pattern, Phase 4 will simplify)
IBaseModel - Use from BaseRepository instead of redundant
findOne({ uid, deletedAt: null })wrappersfindByUid - Only add if it has additional logic (relations, scoping)
findByUid - 🔴 Critical: Never implement — let Controller call
findByUidOrThrowensureResourceExists() - 🔴 Critical: Always filter in custom queries
deletedAt: null - Use for pagination (count + data)
Promise.all - Return for not found (never throw from repository for "not found")
null - 🔴 Critical: Never throw HTTP Exceptions (leave that to Controller via )
ensureResourceExists - Use for typed relations
Prisma.GetPayload - Implement for versioned entities
updateWithVersionCheck - Throw (not HTTP exceptions)
VersionConflictError - Disambiguate P2025: 404 (not found) vs 409 (version conflict)
- Implement for complex filtering scenarios
findPaginated - Accept domain-level parameters (not Prisma types) in public methods
- Use when filtering by non-unique fields like
findFirstdeletedAt
- 🔴 关键要求:继承(绝不要从零开始实现仓库)
BaseRepository - 创建实现的正确ModelWrapper(当前模式,第4阶段将简化)
IBaseModel - 使用BaseRepository中的,而非冗余的
findOne({ uid, deletedAt: null })包装方法findByUid - 仅当需要额外逻辑(关联查询、作用域)时,才添加方法
findByUid - 🔴 关键要求:绝不要实现— 让控制器调用
findByUidOrThrowensureResourceExists() - 🔴 关键要求:在自定义查询中始终过滤
deletedAt: null - 使用实现分页(统计数 + 数据)
Promise.all - 未找到记录时返回(绝不要在仓库中抛出"未找到"异常)
null - 🔴 关键要求:绝不要抛出HTTP异常(交由控制器通过处理)
ensureResourceExists - 使用实现类型化关联
Prisma.GetPayload - 为带版本的实体实现
updateWithVersionCheck - 抛出(而非HTTP异常)
VersionConflictError - 区分P2025错误:404(未找到)与409(版本冲突)
- 为复杂过滤场景实现
findPaginated - 公开方法接收领域级参数(而非Prisma类型)
- 当按非唯一字段(如)过滤时,使用
deletedAtfindFirst
Related Skills
相关技能
- Service Pattern NestJS - Service layer using repositories
- Database Patterns - Soft delete, transactions, optimistic locking
- Backend Controller Pattern NestJS - Controller patterns
- Service Pattern NestJS - 使用仓库的服务层模式
- Database Patterns - 软删除、事务、乐观锁
- Backend Controller Pattern NestJS - 控制器模式