repository-pattern-nestjs

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Repository 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:
  1. Encapsulate queries - All database operations go through repositories
  2. Hide database details - Services don't know SQL or ORM specifics
  3. Provide typed interfaces - Strong typing for queries and results
  4. Handle errors - Low-level errors are handled here
  5. Implement soft deletes - Consistent deletion strategy
  6. Support common patterns - Find, create, update, delete operations
  7. Optimize queries - Eager loading, indexes, pagination

仓库作为数据访问的抽象层,应具备以下能力:
  1. 封装查询 - 所有数据库操作都通过仓库执行
  2. 隐藏数据库细节 - 服务层无需了解SQL或ORM的具体实现
  3. 提供类型化接口 - 为查询和结果提供强类型支持
  4. 处理错误 - 在这一层处理底层错误
  5. 实现软删除 - 保持一致的删除策略
  6. 支持通用操作 - 实现查询、创建、更新、删除等常见操作
  7. 优化查询 - 预加载、索引、分页等优化手段

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
    deletedAt: null
    filtering in base methods
  • 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 a
ModelWrapper
class that bridges
BaseRepository
generics 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.
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
类,用于连接
BaseRepository
泛型与Prisma委托。 该包装类不具备业务价值,将在后续阶段(第4阶段)简化为直接注入PrismaService。 目前请遵循此模式,以保持与现有代码库的一致性。
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 implement
findByUidOrThrow
in repositories. The Controller-Checks Pattern requires services to return
null
and controllers to call
ensureResourceExists()
. Throwing in the repository bypasses this pattern and couples the data layer to HTTP semantics. Use
findOne({ uid, deletedAt: null })
and let the controller handle the 404.

🟡 推荐实践:在此处实现领域特定的查询逻辑(而非在服务层)。
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
,由控制器调用
ensureResourceExists()
。 在仓库中抛出异常会绕过此模式,并将数据层与HTTP语义耦合。 请使用
findOne({ uid, deletedAt: null })
,并由控制器处理404错误。

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
updateWithVersionCheck()
for versioned entities.
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
    BaseRepository
    (never implement repositories from scratch)
  • Create proper ModelWrapper implementing
    IBaseModel
    (current pattern, Phase 4 will simplify)
  • Use
    findOne({ uid, deletedAt: null })
    from BaseRepository instead of redundant
    findByUid
    wrappers
  • Only add
    findByUid
    if it has additional logic (relations, scoping)
  • 🔴 Critical: Never implement
    findByUidOrThrow
    — let Controller call
    ensureResourceExists()
  • 🔴 Critical: Always filter
    deletedAt: null
    in custom queries
  • Use
    Promise.all
    for pagination (count + data)
  • Return
    null
    for not found (never throw from repository for "not found")
  • 🔴 Critical: Never throw HTTP Exceptions (leave that to Controller via
    ensureResourceExists
    )
  • Use
    Prisma.GetPayload
    for typed relations
  • Implement
    updateWithVersionCheck
    for versioned entities
  • Throw
    VersionConflictError
    (not HTTP exceptions)
  • Disambiguate P2025: 404 (not found) vs 409 (version conflict)
  • Implement
    findPaginated
    for complex filtering scenarios
  • Accept domain-level parameters (not Prisma types) in public methods
  • Use
    findFirst
    when filtering by non-unique fields like
    deletedAt

  • 🔴 关键要求:继承
    BaseRepository
    (绝不要从零开始实现仓库)
  • 创建实现
    IBaseModel
    的正确ModelWrapper(当前模式,第4阶段将简化)
  • 使用BaseRepository中的
    findOne({ uid, deletedAt: null })
    ,而非冗余的
    findByUid
    包装方法
  • 仅当需要额外逻辑(关联查询、作用域)时,才添加
    findByUid
    方法
  • 🔴 关键要求:绝不要实现
    findByUidOrThrow
    — 让控制器调用
    ensureResourceExists()
  • 🔴 关键要求:在自定义查询中始终过滤
    deletedAt: null
  • 使用
    Promise.all
    实现分页(统计数 + 数据)
  • 未找到记录时返回
    null
    (绝不要在仓库中抛出"未找到"异常)
  • 🔴 关键要求:绝不要抛出HTTP异常(交由控制器通过
    ensureResourceExists
    处理)
  • 使用
    Prisma.GetPayload
    实现类型化关联
  • 为带版本的实体实现
    updateWithVersionCheck
  • 抛出
    VersionConflictError
    (而非HTTP异常)
  • 区分P2025错误:404(未找到)与409(版本冲突)
  • 为复杂过滤场景实现
    findPaginated
  • 公开方法接收领域级参数(而非Prisma类型)
  • 当按非唯一字段(如
    deletedAt
    )过滤时,使用
    findFirst

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 - 控制器模式