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

NestJS中Mongoose三层嵌套字段填充失败及相关问题排查

NestJS + Mongoose 嵌套模型关联填充问题排查与解决

我在NestJS应用中使用Mongoose定义了三个嵌套关系的模型:User 嵌套 Partners,Partners 嵌套 PartnerRequest。需求是通过 partnerId 将 User 模型的 name 和 shortCode 字段同步填充到 PartnerRequest 中,但目前遇到三个问题,先贴出三个模型的代码:

模型代码

user.model.ts

// user.model.ts
import { Schema, Prop, SchemaFactory } from '@nestjs/mongoose'
import mongoose, { Document } from 'mongoose'

import { Partners, PartnersSchema } from './partners.model'

@Schema({
  toJSON: {
    virtuals: true,
  },
  timestamps: true,
})
export class User {
  @Prop()
  name: string

  @Prop()
  shortCode: string

  @Prop({ type: PartnersSchema })
  partners: Partners
}

const UserSchema = SchemaFactory.createForClass(User)

partners.model.ts

import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'

import { PartnerHistoryEntry, PartnerHistoryEntrySchema } from './partner-history-entry.model'
import { PartnerRequest, PartnerRequestSchema } from './partner-request.model'

@Schema({
  _id: false,
  toJSON: {
    virtuals: true,
  },
  toObject: {
    virtuals: true,
  },
})
export class Partners {
  @Prop({ type: [PartnerRequestSchema], default: [] })
  requests: PartnerRequest[]

  @Prop({ type: [PartnerHistoryEntrySchema], default: [] })
  history: PartnerHistoryEntry[]
}

const PartnersSchema = SchemaFactory.createForClass(Partners)

export { PartnersSchema }

partner-request.model.ts(原错误实现)

import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'
import mongoose from 'mongoose'

// eslint-disable-next-line import/no-cycle
import { User } from './user.model'

@Schema({
  timestamps: true,
  _id: false,
  toJSON: {
    virtuals: true,
  },
  toObject: {
    virtuals: true,
  },
})
export class PartnerRequest {
  @Prop()
  partnerId: mongoose.Types.ObjectId

  @Prop()
  partnerShortCode: string

  @Prop()
  partnerName: string
}

const PartnerRequestSchema = SchemaFactory.createForClass(PartnerRequest)

PartnerRequestSchema.virtual('partnerName', {
  ref: User.name,
  localField: 'partnerId',
  foreignField: '_id',
  justOne: true,
})
PartnerRequestSchema.virtual('partnerShortCode', {
  ref: User.shortCode,
  localField: 'partnerId',
  foreignField: '_id',
  justOne: true,
})

PartnerRequestSchema.pre(
  ['find', 'findOne', 'count', 'findOneAndDelete', 'findOneAndRemove', 'findOneAndUpdate'],
  // eslint-disable-next-line func-names
  function () {
    this.populate('partnerName')
    this.populate('partnerShortCode')
  }
)

export { PartnerRequestSchema }

遇到的问题

  1. 循环依赖:partner-request.model.ts 导入 User,user.model.ts 通过 partners.model 间接导入 PartnerRequest,触发ESLint的import/no-cycle错误,目前靠注释禁用检测;
  2. 类型错误:定义虚拟字段partnerShortCode时使用ref: User.shortCode,报错“Property 'shortCode' does not exist on type 'typeof User'.ts(2339)”;
  3. 填充失效:接口返回的requests数组中partnerName和partnerShortCode字段缺失,未被填充。

问题解决

1. 循环依赖:用字符串引用替代类导入

直接移除对User类的导入,改用Mongoose支持的模型名称字符串作为关联引用,彻底消除循环依赖:

// partner-request.model.ts 中删除此行
// import { User } from './user.model'

// 虚拟字段的ref改为字符串'User'(对应User模型的Schema名称)
PartnerRequestSchema.virtual('partner', {
  ref: 'User',
  localField: 'partnerId',
  foreignField: '_id',
  justOne: true,
})

2. 类型错误:正确定义虚拟关联与字段映射

Mongoose的ref只能指定关联模型名称,不能直接指定模型字段。需先定义指向User的虚拟关联,再通过getter或transform提取目标字段:

方式1:用getter生成字段

// partner-request.model.ts
PartnerRequestSchema.virtual('partner', {
  ref: 'User',
  localField: 'partnerId',
  foreignField: '_id',
  justOne: true,
})

// 添加getter从关联的partner中提取字段
PartnerRequestSchema.path('partnerName').get(function(this: any) {
  return this.partner?.name;
});

PartnerRequestSchema.path('partnerShortCode').get(function(this: any) {
  return this.partner?.shortCode;
});

方式2:在JSON转换时处理

@Schema({
  timestamps: true,
  _id: false,
  toJSON: {
    virtuals: true,
    transform: function(doc, ret) {
      ret.partnerName = ret.partner?.name;
      ret.partnerShortCode = ret.partner?.shortCode;
      // 可选:移除临时的partner字段
      delete ret.partner;
      return ret;
    }
  },
  toObject: {
    virtuals: true,
    transform: function(doc, ret) {
      ret.partnerName = ret.partner?.name;
      ret.partnerShortCode = ret.partner?.shortCode;
      delete ret.partner;
      return ret;
    }
  },
})
export class PartnerRequest {
  @Prop()
  partnerId: mongoose.Types.ObjectId

  // 移除实体字段定义,添加类型声明即可
  partnerName?: string;
  partnerShortCode?: string;
}

3. 填充失效:修复嵌套模型的填充逻辑

核心原因

  • 子文档的pre中间件不会在查询父文档时触发;
  • 原代码中partnerName和partnerShortCode被定义为实体字段,覆盖了虚拟字段。

解决步骤

  1. 移除实体字段定义:删除PartnerRequest类中partnerName和partnerShortCode的@Prop()装饰器,仅保留类型声明;
  2. 手动指定嵌套填充路径:在查询User时,明确填充嵌套的partner关联:
// User服务层查询示例
await UserModel.find().populate({
  path: 'partners.requests.partner',
  model: 'User'
});
  1. 全局生效方案:在UserSchema上添加pre中间件,自动处理填充:
// user.model.ts
const UserSchema = SchemaFactory.createForClass(User);

UserSchema.pre(['find', 'findOne'], function() {
  this.populate({
    path: 'partners.requests.partner',
    model: 'User'
  });
});

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 01:52:58