NestJS/Mongoose中如何基于关联的外部Schema数据使用虚拟属性计算字段
NestJS/Mongoose中如何基于关联的外部Schema数据使用虚拟属性计算字段
我来帮你搞定这个问题!你遇到的核心问题是对NestJS/Mongoose的虚拟属性逻辑理解有偏差:本地虚拟属性(只依赖当前文档字段,比如你之前写的totalExclTax)会自动计算,但关联型虚拟属性(依赖其他集合的文档,比如你需要的InvoiceHub数据)不会自动填充关联内容,必须手动触发加载才行。
先拆解下你之前写法的问题:
- 你把关联配置(
ref/localField这些)和计算逻辑get函数混在了同一个@Virtual装饰器里,这不符合Mongoose关联虚拟属性的定义规则; - 你在
get函数里直接写了console.log({ hub }),但这里的hub根本没定义,应该用this.hub才对。
解决方案
我们分两步来处理:先定义一个关联InvoiceHub的虚拟属性,再基于这个关联属性计算amountDue。
1. 修正Bill类的虚拟属性定义
先在Bill类里加一个专门用来关联InvoiceHub的虚拟属性hub,再单独写amountDue的计算逻辑:
@Schema() export class Bill extends OmitType(Invoice, ['type'] as const) { // ... 你已有的其他字段和虚拟属性(比如amountPaid)都保留 @Prop({ type: [PaymentSchema], default: [], set: (val: unknown) => (val === null || val === undefined ? [] : val), }) payments: Payment[]; // 第一步:先定义关联InvoiceHub的虚拟属性 @Virtual({ options: { ref: 'InvoiceHub', // 要关联的集合名称 localField: 'hubId', // Bill里存的关联ID字段 foreignField: '_id', // InvoiceHub对应的主键字段 justOne: true, // 只返回单个关联的Hub文档 }, }) readonly hub?: InvoiceHub; // 第二步:基于关联的Hub计算amountDue @Virtual({ get: function (this: Bill & { hub?: InvoiceHub }) { // 先判断关联的Hub是否加载成功,避免空值报错 if (!this.hub) return 0; return this.hub.totalInclTax - this.amountPaid; }, }) readonly amountDue: number; // ... 其他已有代码保留 }
2. 查询时手动填充关联数据
Mongoose的关联虚拟属性不会自动加载,必须在查询的时候调用.populate()方法触发填充。所以你需要修改测试代码里的查询逻辑:
describe.only('payments', () => { it.only('should recalculate amountPaid, amountDue and paymentStatus when payments added', async () => { const hub = await InvoiceHubModel.create(generateInvoiceHubSchema()); const items = Array.from({ length: 10 }, () => generateItemSchema()); hub.items.push(...items); await hub.save(); // 1. 先创建Bill文档 const bill = await BillModel.create({ ...generateBillSchema(), hubId: hub._id, status: BillStatus.APPROVED }); // 2. 重新查询Bill并填充关联的hub虚拟属性 const populatedBill = await BillModel.findById(bill._id).populate('hub').exec(); // 3. 现在断言就能拿到正确的amountDue了 expect(populatedBill.amountDue).toEqual(hub.totalInclTax); }); });
3. 可选:设置自动填充(省得每次手动写populate)
如果你希望每次查询Bill时都自动加载hub数据,可以给BillSchema加个预查询钩子:
export const BillSchema = SchemaFactory.createForClass(Bill); // 给find和findOne查询自动加上populate BillSchema.pre('find', function() { this.populate('hub'); }); BillSchema.pre('findOne', function() { this.populate('hub'); });
这样后续查询就不用每次手动写.populate('hub')了,但要注意:如果某些查询不需要hub数据,这会带来额外的性能开销,所以按需启用就好。
关键提醒
- 关联型虚拟属性是查询阶段的操作:创建文档的时候不会自动填充关联内容,必须在查询时触发
populate,才能在计算虚拟属性时拿到InvoiceHub的数据; - 虚拟属性有依赖顺序:确保
amountDue的定义在hub虚拟属性之后,NestJS/Mongoose会按顺序处理这些属性; - 类型安全:用
this: Bill & { hub?: InvoiceHub }给this补全类型,避免TypeScript报错。
这样改完之后,你的测试肯定能通过,amountDue也会正确基于InvoiceHub的totalInclTax计算啦!
内容来源于stack exchange
相关产品推荐
相关产品推荐

