NestJS中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

