白留白WHITE SPACE
返回所有文章
NestJS / 后端开发

NestJS 实战 03:Repository 抽象与可替换存储

把 UsersService 从“直接操作数组”升级为“依赖仓储接口”。引入 UsersRepository 抽象,默认使用内存实现。为后续切换数据库(Prisma/TypeORM)做无痛准备。

本文属于 NestJS BBS 实战教程,共 11 节,建议按顺序学习。

1. 这节课要实现的功能是什么

  • 把 UsersService 从“直接操作数组”升级为“依赖仓储接口”。
  • 引入 UsersRepository 抽象,默认使用内存实现。
  • 为后续切换数据库(Prisma/TypeORM)做无痛准备。

2. 这节课涉及的 NestJS 知识点有哪些(先讲概念)

2.1 为什么要引入 Repository 抽象

Service 的职责是业务规则,不应该绑定具体存储细节。把存储抽象成仓储接口后:

  • 业务层与存储层解耦;
  • 测试可以替换为 mock/fake;
  • 换数据库时,Controller 和大部分业务逻辑不用动。

2.2 Provider Token 是什么

接口在运行时不存在,所以 Nest 不能直接按接口注入。要用一个 Token(字符串或 Symbol)作为注入标识。

export const USERS_REPOSITORY = Symbol('USERS_REPOSITORY');

2.3 useClass 的作用

useClass 表示“当有人注入这个 Token 时,用哪个具体类实例化”。

{
  provide: USERS_REPOSITORY,
  useClass: InMemoryUsersRepository,
}

2.4 设计注意点

  • Token 建议用 Symbol,避免命名冲突;
  • Service 依赖 Token,不依赖具体类;
  • 模块中统一管理绑定关系,避免散落在业务代码里。

3. 实际开发中这些 NestJS 知识点被如何使用(详细代码)

3.1 src/users/users.repository.ts - 定义 Token 与仓储契约

export const USERS_REPOSITORY = Symbol('USERS_REPOSITORY');

export type UserRole = 'user' | 'moderator' | 'admin';

export interface UserEntity {
  id: number;
  username: string;
  email: string;
  passwordHash: string;
  role: UserRole;
  createdAt: Date;
}

export interface UsersRepository {
  create(data: Omit<UserEntity, 'id' | 'createdAt'>): Promise<UserEntity>;
  findAll(): Promise<UserEntity[]>;
  findById(id: number): Promise<UserEntity | null>;
  findByEmail(email: string): Promise<UserEntity | null>;
  updateRole(id: number, role: UserRole): Promise<UserEntity | null>;
}

3.2 src/users/repositories/in-memory-users.repository.ts - 默认实现

import { Injectable } from '@nestjs/common';
import { UserEntity, UserRole, UsersRepository } from '../users.repository';

@Injectable()
export class InMemoryUsersRepository implements UsersRepository {
  private readonly users: UserEntity[] = [];
  private nextId = 1;

  async create(data: Omit<UserEntity, 'id' | 'createdAt'>): Promise<UserEntity> {
    const user: UserEntity = { id: this.nextId++, createdAt: new Date(), ...data };
    this.users.push(user);
    return user;
  }

  async findAll(): Promise<UserEntity[]> {
    return [...this.users];
  }

  async findById(id: number): Promise<UserEntity | null> {
    return this.users.find((user) => user.id === id) ?? null;
  }

  async findByEmail(email: string): Promise<UserEntity | null> {
    return this.users.find((user) => user.email === email) ?? null;
  }

  async updateRole(id: number, role: UserRole): Promise<UserEntity | null> {
    const user = this.users.find((item) => item.id === id);
    if (!user) return null;
    user.role = role;
    return user;
  }
}

3.3 src/users/users.module.ts - 绑定 Token 与实现

import { Module } from '@nestjs/common';
import { InMemoryUsersRepository } from './repositories/in-memory-users.repository';
import { UsersController } from './users.controller';
import { USERS_REPOSITORY } from './users.repository';
import { UsersService } from './users.service';

@Module({
  controllers: [UsersController],
  providers: [
    UsersService,
    InMemoryUsersRepository,
    {
      provide: USERS_REPOSITORY,
      useClass: InMemoryUsersRepository,
    },
  ],
  exports: [UsersService, USERS_REPOSITORY],
})
export class UsersModule {}

3.4 为什么这里会同时出现“类 Token”和“自定义 Token”

这里是最容易疑惑的一段代码。先看结论:两者都可以存在,但职责不同。

  • 类 Token(InMemoryUsersRepository)用于“按具体类注入”;
  • 自定义 Token(USERS_REPOSITORY)用于“按抽象契约注入”。

所以在 providers 里同时注册它们,是为了兼容两种注入方式。
但如果写成 useClass: InMemoryUsersRepository,Nest 会按两个 Token 分别实例化,可能产生双实例。

如果你希望两种注入方式共享同一个仓储实例,建议改为:

@Module({
  providers: [
    UsersService,
    InMemoryUsersRepository,
    {
      provide: USERS_REPOSITORY,
      useExisting: InMemoryUsersRepository,
    },
  ],
})
export class UsersModule {}

3.5 src/users/users.service.ts - 注入抽象而非具体类

import { Inject, Injectable } from '@nestjs/common';
import { USERS_REPOSITORY } from './users.repository';
import type { UsersRepository } from './users.repository';

@Injectable()
export class UsersService {
  constructor(
    @Inject(USERS_REPOSITORY)
    private readonly usersRepository: UsersRepository,
  ) {}

  findAll() {
    return this.usersRepository.findAll();
  }
}

3.6 本节落地价值

当你后面换成 Prisma 仓储时,只需要替换 useClass 绑定,Service 层几乎不用改。这就是可演进架构的核心收益。

4. 总结这节课知识点

  • 你学会了用 Token + Provider 绑定做“依赖可替换”。
  • 你理解了类 Token 与自定义 Token 可以并存,以及如何避免双实例。
  • 你把业务和存储彻底解耦,后续扩展成本大幅下降。
  • 你掌握了中大型 Nest 项目最关键的结构能力之一。

5. 每节课分支

  • lesson-03-repository-pattern
  • 远程仓库:https://github.com/Shiuwn/nestjs-tutorial.git

返回课程目录

上一课:DTO 与 ValidationPipe 参数校验

下一课:ConfigModule 与基础数据库生命周期

— 感谢阅读 —