Mongoose populate()关联查询Order文档时返回空数组
Mongoose populate() 关联字段返回空数组修复
问题现象
调用Mongoose populate() 填充Order文档的food、drinks关联字段时,接口始终返回空数组;移除populate()调用时,可正常读取两个字段存储的ObjectId数组。
现有代码与运行信息
Schema定义
- Menu.js
注意:Menu模型的food、drinks字段直接嵌套
Food.schema、Drink.schema作为子文档存储,未配置ref属性。const mongoose = require("mongoose"); const { Schema } = mongoose; const Food = require("./Food"); const Drink = require("./Drink"); const MenuSchema = new Schema({ code: { type: mongoose.ObjectId, required: true, unique: true, }, name: { type: String, required: true, }, food: { type: [Food.schema], required: false, }, drinks: { type: [Drink.schema], required: false, }, type: { type: String, }, createdAt: { type: Date, default: Date.now, }, }); module.exports = mongoose.model("Menu", MenuSchema); - Food.js
const mongoose = require("mongoose"); const { Schema } = mongoose; const FoodSchema = new Schema({ code: { type: mongoose.ObjectId, required: true, unique: true, }, description: { type: String, required: true, }, ingredients: { type: [], required: false, default: [], }, stock: { type: Number, required: true, }, type: { type: String, enum: ["starter", "main", "dessert"], required: true, }, price: { type: Number, required: true, }, createdAt: { type: Date, default: Date.now, }, }); module.exports = mongoose.model("Food", FoodSchema); - Drink.js
const mongoose = require("mongoose"); const { Schema } = mongoose; const DrinkSchema = new Schema({ code: { type: mongoose.ObjectId, required: true, unique: true, }, description: { type: String, required: true, }, stock: { type: Number, required: true, }, price: { type: Number, required: true, }, createdAt: { type: Date, default: Date.now, }, }); module.exports = mongoose.model("Drink", DrinkSchema); - Order.js
const mongoose = require("mongoose"); const { Schema } = mongoose; const OrderSchema = new Schema({ code: { type: mongoose.ObjectId, required: true, unique: true, }, type: { type: String, required: true, enum: ["restaurant", "takeaway", "delivery"], }, food: { type: [{ type: mongoose.ObjectId, ref: "Food" }], required: false, default: [], }, drinks: { type: [{ type: mongoose.ObjectId, ref: "Drink" }], required: false, default: [], }, orderValue: Number, client_id: { type: mongoose.ObjectId, ref: "Client", required: false, }, createdAt: { type: Date, default: Date.now, }, }); module.exports = mongoose.model("Order", OrderSchema);
控制器查询逻辑
查询逻辑位于TableController.js:
exports.getOrderFromTable = async (req, res) => { const { code, tableCode } = req.params; try { const foundRestaurant = await Restaurant.findOne({ code: code }); const foundTable = foundRestaurant.tables.filter( (table) => table.code == tableCode ); console.log(foundTable[0].order_id); const foundOrder = await Order.findOne({ _id: foundTable[0].order_id, }) .populate("food") .populate("drinks"); res.send(foundOrder); } catch (err) { res.json({ message: err }); } };
Mongoose调试日志
Mongoose: restaurants.findOne({ code: new ObjectId("62a60dcb9fe25d276815675c") }, { projection: {} }) new ObjectId("62a60ece9fe25d27681567b7") Mongoose: orders.findOne({ _id: new ObjectId("62a60ece9fe25d27681567b7") }, { projection: {} }) Mongoose: foods.find({ _id: { '$in': [ new ObjectId("62a60e039fe25d276815677d"), new ObjectId("62a60e169fe25d2768156785") ], [Symbol(mongoose#trustedSymbol)]: true }}, { skip: undefined, limit: undefined, perDocumentLimit: undefined, projection: {}}) Mongoose: drinks.find({ _id: { '$in': [ new ObjectId("62a60e259fe25d276815678d"), new ObjectId("62a60e3c9fe25d276815679c") ], [Symbol(mongoose#trustedSymbol)]: true }}, { skip: undefined, limit: undefined, perDocumentLimit: undefined, projection: {}})
接口返回结果
{ "_id": "62a60ece9fe25d27681567b7", "code": "62a60ece9fe25d27681567b6", "type": "restaurant", "food": [], "drinks": [], "orderValue": 25, "createdAt": "2022-06-12T16:05:34.513Z", "__v": 0 }
根因分析
从调试日志可以确认populate()逻辑已经正常触发,向foods、drinks集合发起了关联查询,返回空数组的核心原因是查询条件匹配不到对应文档,结合Schema设计,对应两种常见场景:
- Order中存储的是Food/Drink文档自定义的
code字段值,而非Mongoose默认的_id字段。默认populate逻辑会用本地字段值匹配关联集合的_id,自然无法命中。 - Order中存储的是Menu下嵌套food/drinks子文档自动生成的
_id。由于Menu的food、drinks是直接嵌套的子文档,这些子文档仅从属于Menu文档,其_id不存在于独立的foods/drinks集合中,关联查询自然返回空。
修复方案
根据实际业务场景选择对应修复方式:
场景1:关联字段使用自定义code匹配
如果业务设计就是用code作为关联唯一键,修改populate配置,手动指定关联字段映射:
const foundOrder = await Order.findOne({ _id: foundTable[0].order_id, }) .populate({ path: "food", localField: "food", foreignField: "code" // 指定关联Food集合的匹配字段为code }) .populate({ path: "drinks", localField: "drinks", foreignField: "code" // 指定关联Drink集合的匹配字段为code });
场景2:存储的ID不属于独立关联集合
- 快速修复:创建Order写入food、drinks字段时,必须存入
foods/drinks独立集合中真实存在的文档_id(或code,对应场景1配置),不要取Menu嵌套子文档自动生成的_id。 - 架构统一修复:修改Menu Schema,将food、drinks从嵌套子文档改为和Order一致的引用模式,从根源上避免ID混淆:
// 修改后的MenuSchema对应字段 food: { type: [{ type: mongoose.ObjectId, ref: "Food" }], required: false, default: [] }, drinks: { type: [{ type: mongoose.ObjectId, ref: "Drink" }], required: false, default: [] },
修复后可单独执行Food.find({_id: {$in: Order中存储的food数组}})验证查询结果,确认能命中文档后,populate即可正常返回填充数据。
内容的提问来源于stack exchange,提问作者Rui Castro
相关产品推荐
相关产品推荐

