You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

NestJS中Repository模式的正确实现及嵌套文档操作难题

NestJS + MongoDB 架构优化:Repository模式实现与解耦方案

问题背景

分析NestJS官方文档及示例代码后发现,官方直接在Service类(如CatsService)中执行数据库操作,未引入Repository模式,引发以下核心问题:

  • 数据库类型变更困难:数据库操作逻辑分散在多个Service中,更换数据库需修改大量代码
  • 跨层类型污染:Mongo文档模型被复用在数据库Schema、Service、Controller/Resolver等多层,导致TypeScript类型冲突
  • 职责边界模糊:Service层中业务逻辑与数据库操作代码混杂,不利于维护和测试

官方示例的问题体现

1. 直接操作Mongo Model的Service

官方CatsService直接注入并使用Mongoose的Model,将数据库操作暴露给上层:

import { Model } from 'mongoose';
import { Injectable, Inject } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';
import { CreateCatDto } from './dto/create-cat.dto';

@Injectable()
export class CatsService {
  constructor(
    @Inject('CAT_MODEL')
    private catModel: Model<Cat>,
  ) {}

  async create(createCatDto: CreateCatDto): Promise<Cat> {
    const createdCat = new this.catModel(createCatDto);
    return createdCat.save();
  }

  async findAll(): Promise<Cat[]> {
    return this.catModel.find().exec();
  }
}

2. Resolver/Controller与数据库强耦合

GraphQL的CatsResolver直接接收Service返回的Mongo相关类型,导致Resolver与数据库实现绑定:

import { ParseIntPipe, UseGuards } from '@nestjs/common';
import { Args, Mutation, Query, Resolver, Subscription } from '@nestjs/graphql';
import { PubSub } from 'graphql-subscriptions';
import { Cat } from '../graphql.schema';
import { CatsGuard } from './cats.guard';
import { CatsService } from './cats.service';
import { CreateCatDto } from './dto/create-cat.dto';

const pubSub = new PubSub();

@Resolver('Cat')
export class CatsResolver {
  constructor(private readonly catsService: CatsService) {}

  @Query('cats')
  @UseGuards(CatsGuard)
  async getCats() {
    return this.catsService.findAll();
  }
}

3. REST场景下的TypeScript类型冲突

当业务模型与Mongo文档模型分离时,强制转换会触发类型错误:

import { Customer } from './models/customer.model';
import { CustomerService } from './customer.service';

@Controller('customer')
export class CustomerController {
  constructor(
    private customerService: CustomerService,
  ) {}

  @Get(':id')
  async getCustomer(@Param('id') id: string): Promise<Customer> {
    const customer = await this.customerService.findById(id) as Customer;
    return customer;
  }
}

错误提示:

Conversion of type
'import("c:/Test/src/customer/customer.schema").Customer' to type
'import("c:/Test/src/customer/models/customer.model"). Customer' may
be a mistake because neither type sufficiently overlaps with the
other. If this was intentional, convert the expression to 'unknown'
first.


解决方案:MongoDB场景下的Repository模式实现

1. 分层架构设计

明确各层职责,实现彻底解耦:

  • Controller/Resolver:处理HTTP/GraphQL请求,调用Service,返回业务模型
  • Service层:仅处理业务逻辑,依赖Repository接口,不直接操作数据库
  • Repository层:封装所有数据库操作,实现Repository接口,屏蔽底层MongoDB细节
  • 模型层:严格区分业务模型(供上层使用)和数据库模型(仅Repository层使用)

2. 通用Repository接口与MongoDB基础实现

先定义通用CRUD接口,再基于Mongoose实现基础Mongo操作:

通用BaseRepository接口

export interface BaseRepository<T> {
  create(entity: Partial<T>): Promise<T>;
  findAll(): Promise<T[]>;
  findById(id: string): Promise<T | null>;
  updateById(id: string, entity: Partial<T>): Promise<T | null>;
  deleteById(id: string): Promise<boolean>;
}

MongoDB基础实现类

import { Model, Document } from 'mongoose';
import { BaseRepository } from './base.repository';

export abstract class MongoBaseRepository<T extends Document> implements BaseRepository<T> {
  constructor(protected readonly model: Model<T>) {}

  async create(entity: Partial<T>): Promise<T> {
    const createdEntity = new this.model(entity);
    return createdEntity.save();
  }

  async findAll(): Promise<T[]> {
    return this.model.find().exec();
  }

  async findById(id: string): Promise<T | null> {
    return this.model.findById(id).exec();
  }

  async updateById(id: string, entity: Partial<T>): Promise<T | null> {
    return this.model.findByIdAndUpdate(id, entity, { new: true }).exec();
  }

  async deleteById(id: string): Promise<boolean> {
    const result = await this.model.findByIdAndDelete(id).exec();
    return !!result;
  }
}

3. 扩展Repository支持嵌套文档操作

针对MongoDB嵌套文档(如客户的地址数组),在具体业务Repository中添加专属方法:

CustomerRepository接口(含嵌套操作)

import { BaseRepository } from './base.repository';
import { CustomerDocument } from '../schemas/customer.schema';
import { Address } from '../models/address.model';

export interface CustomerRepository extends BaseRepository<CustomerDocument> {
  addAddress(customerId: string, address: Address): Promise<CustomerDocument | null>;
  updateAddress(customerId: string, addressId: string, address: Partial<Address>): Promise<CustomerDocument | null>;
  removeAddress(customerId: string, addressId: string): Promise<CustomerDocument | null>;
}

MongoDB实现类

import { Injectable } from '@nestjs/common';
import { Model } from 'mongoose';
import { InjectModel } from '@nestjs/mongoose';
import { MongoBaseRepository } from './mongo-base.repository';
import { CustomerRepository } from './customer.repository';
import { CustomerDocument } from '../schemas/customer.schema';
import { Address } from '../models/address.model';

@Injectable()
export class CustomerMongoRepository extends MongoBaseRepository<CustomerDocument> implements CustomerRepository {
  constructor(@InjectModel('Customer') private readonly customerModel: Model<CustomerDocument>) {
    super(customerModel);
  }

  async addAddress(customerId: string, address: Address): Promise<CustomerDocument | null> {
    return this.customerModel.findByIdAndUpdate(
      customerId,
      { $push: { addresses: address } },
      { new: true }
    ).exec();
  }

  async updateAddress(customerId: string, addressId: string, address: Partial<Address>): Promise<CustomerDocument | null> {
    return this.customerModel.findOneAndUpdate(
      { _id: customerId, 'addresses._id': addressId },
      { $set: { 'addresses.$': { ...address, _id: addressId } } },
      { new: true }
    ).exec();
  }

  async removeAddress(customerId: string, addressId: string): Promise<CustomerDocument | null> {
    return this.customerModel.findByIdAndUpdate(
      customerId,
      { $pull: { addresses: { _id: addressId } } },
      { new: true }
    ).exec();
  }
}

4. 层间解耦实现

模型分离与映射

定义独立的业务模型和数据库模型,通过映射函数转换:

// 业务模型(供上层使用)
export interface Customer {
  id: string;
  name: string;
  addresses: Address[];
}

// 数据库模型(仅Repository层使用)
import { Document } from 'mongoose';
export interface CustomerDocument extends Document {
  name: string;
  addresses: (Address & { _id: string })[];
}

// 模型映射函数
export const mapCustomerDocumentToCustomer = (doc: CustomerDocument): Customer => ({
  id: doc._id.toString(),
  name: doc.name,
  addresses: doc.addresses.map(addr => ({
    id: addr._id.toString(),
    street: addr.street,
    city: addr.city
  }))
});

Service依赖Repository接口

Service注入抽象接口而非具体实现,降低耦合:

import { Injectable, Inject } from '@nestjs/common';
import { CustomerRepository } from './repositories/customer.repository';
import { Customer } from './models/customer.model';
import { CreateCustomerDto } from './dto/create-customer.dto';
import { mapCustomerDocumentToCustomer } from './mappers/customer.mapper';

@Injectable()
export class CustomerService {
  constructor(
    @Inject('CustomerRepository') private readonly customerRepository: CustomerRepository
  ) {}

  async create(createCustomerDto: CreateCustomerDto): Promise<Customer> {
    const doc = await this.customerRepository.create(createCustomerDto);
    return mapCustomerDocumentToCustomer(doc);
  }

  async findById(id: string): Promise<Customer | null> {
    const doc = await this.customerRepository.findById(id);
    return doc ? mapCustomerDocumentToCustomer(doc) : null;
  }

  async addAddress(customerId: string, address: Address): Promise<Customer | null> {
    const doc = await this.customerRepository.addAddress(customerId, address);
    return doc ? mapCustomerDocumentToCustomer(doc) : null;
  }
}

Controller返回业务模型

不再需要强制类型转换,直接返回映射后的业务模型:

import { Controller, Get, Param } from '@nestjs/common';
import { CustomerService } from './customer.service';
import { Customer } from './models/customer.model';

@Controller('customer')
export class CustomerController {
  constructor(private readonly customerService: CustomerService) {}

  @Get(':id')
  async getCustomer(@Param('id') id: string): Promise<Customer | null> {
    return this.customerService.findById(id);
  }
}

5. 依赖注入绑定

在模块中绑定Repository接口与Mongo实现类:

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { CustomerController } from './customer.controller';
import { CustomerService } from './customer.service';
import { CustomerSchema } from './schemas/customer.schema';
import { CustomerMongoRepository } from './repositories/customer-mongo.repository';

@Module({
  imports: [MongooseModule.forFeature([{ name: 'Customer', schema: CustomerSchema }])],
  controllers: [CustomerController],
  providers: [
    CustomerService,
    {
      provide: 'CustomerRepository',
      useClass: CustomerMongoRepository
    }
  ]
})
export class CustomerModule {}

效果验证

  • 数据库易替换:只需新增对应数据库的Repository实现类,修改模块绑定即可,Service和Controller无需改动
  • 类型冲突解决:业务模型与数据库模型完全分离,TypeScript类型检查正常
  • 职责清晰:Service专注业务逻辑,Repository专注数据库操作,代码维护性大幅提升
  • 嵌套文档支持:通过扩展Repository方法,实现嵌套文档的精准增删改操作

内容的提问来源于stack exchange,提问作者g_m

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 20:48:19