猫鼬多对多 [英] Many to Many with Mongoose
问题描述
我有两个模型:
Item.js
const mongoose = require('mongoose');
const itemSchema = new mongoose.Schema({
name: String,
stores: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Store' }]
});
const Item = mongoose.model('Item', itemSchema);
module.exports = Item;
Store.js
const mongoose = require('mongoose');
const storeSchema = new mongoose.Schema({
name: String,
items: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Item' }]
});
const Store = mongoose.model('Store', storeSchema);
module.exports = Store;
和一个 seed.js 文件:
const faker = require('faker');
const Store = require('./models/Store');
const Item = require('./models/Item');
console.log('Seeding..');
let item = new Item({
name: faker.name.findName() + " Item"
});
item.save((err) => {
if (err) return;
let store = new Store({
name: faker.name.findName() + " Store"
});
store.items.push(item);
store.save((err) => {
if (err) return;
})
});
store
与包含1个item
的items
数组一起保存.但是,item
没有stores
.我想念什么?如何自动更新MongoDB/Mongoose中的多对多关系?我习惯了Rails,一切都是自动完成的.
当前存在的问题是,您将引用保存在一个模型中,但没有将其保存在另一个模型中. MongoDB中没有自动参照完整性",并且这种关系"的概念实际上是手动"的事情,实际上.populate()
的情况实际上是一堆额外的查询,用于检索参考信息.这里没有魔术".
正确处理多对多"可以归结为三个选项:
清单1-在两个文档中保留数组
按照当前的设计,缺少的零件将引用的内容同时"存储在相关项目中.要显示清单:
const { Schema } = mongoose = require('mongoose');
mongoose.Promise = global.Promise;
mongoose.set('debug',true);
mongoose.set('useFindAndModify', false);
mongoose.set('useCreateIndex', true);
const uri = 'mongodb://localhost:27017/manydemo',
options = { useNewUrlParser: true };
const itemSchema = new Schema({
name: String,
stores: [{ type: Schema.Types.ObjectId, ref: 'Store' }]
});
const storeSchema = new Schema({
name: String,
items: [{ type: Schema.Types.ObjectId, ref: 'Item' }]
});
const Item = mongoose.model('Item', itemSchema);
const Store = mongoose.model('Store', storeSchema);
const log = data => console.log(JSON.stringify(data,undefined,2))
(async function() {
try {
const conn = await mongoose.connect(uri,options);
// Clean data
await Promise.all(
Object.entries(conn.models).map(([k,m]) => m.deleteMany() )
);
// Create some instances
let [toothpaste,brush] = ['toothpaste','brush'].map(
name => new Item({ name })
);
let [billsStore,tedsStore] = ['Bills','Teds'].map(
name => new Store({ name })
);
// Add items to stores
[billsStore,tedsStore].forEach( store => {
store.items.push(toothpaste); // add toothpaste to store
toothpaste.stores.push(store); // add store to toothpaste
});
// Brush is only in billsStore
billsStore.items.push(brush);
brush.stores.push(billsStore);
// Save everything
await Promise.all(
[toothpaste,brush,billsStore,tedsStore].map( m => m.save() )
);
// Show stores
let stores = await Store.find().populate('items','-stores');
log(stores);
// Show items
let items = await Item.find().populate('stores','-items');
log(items);
} catch(e) {
console.error(e);
} finally {
mongoose.disconnect();
}
})();
这将创建项目"集合:
{
"_id" : ObjectId("59ab96d9c079220dd8eec428"),
"name" : "toothpaste",
"stores" : [
ObjectId("59ab96d9c079220dd8eec42a"),
ObjectId("59ab96d9c079220dd8eec42b")
],
"__v" : 0
}
{
"_id" : ObjectId("59ab96d9c079220dd8eec429"),
"name" : "brush",
"stores" : [
ObjectId("59ab96d9c079220dd8eec42a")
],
"__v" : 0
}
和商店"集合:
{
"_id" : ObjectId("59ab96d9c079220dd8eec42a"),
"name" : "Bills",
"items" : [
ObjectId("59ab96d9c079220dd8eec428"),
ObjectId("59ab96d9c079220dd8eec429")
],
"__v" : 0
}
{
"_id" : ObjectId("59ab96d9c079220dd8eec42b"),
"name" : "Teds",
"items" : [
ObjectId("59ab96d9c079220dd8eec428")
],
"__v" : 0
}
并产生总体输出,例如:
Mongoose: items.deleteMany({}, {})
Mongoose: stores.deleteMany({}, {})
Mongoose: items.insertOne({ name: 'toothpaste', _id: ObjectId("59ab96d9c079220dd8eec428"), stores: [ ObjectId("59ab96d9c079220dd8eec42a"), ObjectId("59ab96d9c079220dd8eec42b") ], __v: 0 })
Mongoose: items.insertOne({ name: 'brush', _id: ObjectId("59ab96d9c079220dd8eec429"), stores: [ ObjectId("59ab96d9c079220dd8eec42a") ], __v: 0 })
Mongoose: stores.insertOne({ name: 'Bills', _id: ObjectId("59ab96d9c079220dd8eec42a"), items: [ ObjectId("59ab96d9c079220dd8eec428"), ObjectId("59ab96d9c079220dd8eec429") ], __v: 0 })
Mongoose: stores.insertOne({ name: 'Teds', _id: ObjectId("59ab96d9c079220dd8eec42b"), items: [ ObjectId("59ab96d9c079220dd8eec428") ], __v: 0 })
Mongoose: stores.find({}, { fields: {} })
Mongoose: items.find({ _id: { '$in': [ ObjectId("59ab96d9c079220dd8eec428"), ObjectId("59ab96d9c079220dd8eec429") ] } }, { fields: { stores: 0 } })
[
{
"_id": "59ab96d9c079220dd8eec42a",
"name": "Bills",
"__v": 0,
"items": [
{
"_id": "59ab96d9c079220dd8eec428",
"name": "toothpaste",
"__v": 0
},
{
"_id": "59ab96d9c079220dd8eec429",
"name": "brush",
"__v": 0
}
]
},
{
"_id": "59ab96d9c079220dd8eec42b",
"name": "Teds",
"__v": 0,
"items": [
{
"_id": "59ab96d9c079220dd8eec428",
"name": "toothpaste",
"__v": 0
}
]
}
]
Mongoose: items.find({}, { fields: {} })
Mongoose: stores.find({ _id: { '$in': [ ObjectId("59ab96d9c079220dd8eec42a"), ObjectId("59ab96d9c079220dd8eec42b") ] } }, { fields: { items: 0 } })
[
{
"_id": "59ab96d9c079220dd8eec428",
"name": "toothpaste",
"__v": 0,
"stores": [
{
"_id": "59ab96d9c079220dd8eec42a",
"name": "Bills",
"__v": 0
},
{
"_id": "59ab96d9c079220dd8eec42b",
"name": "Teds",
"__v": 0
}
]
},
{
"_id": "59ab96d9c079220dd8eec429",
"name": "brush",
"__v": 0,
"stores": [
{
"_id": "59ab96d9c079220dd8eec42a",
"name": "Bills",
"__v": 0
}
]
}
]
关键点在于您实际上将参考数据添加到存在关系的每个集合中的每个文档中.这里使用的数组"用于存储这些引用,并查找"相关集合的结果,并将其替换为存储在其中的对象数据.
请注意以下部分:
// Add items to stores
[billsStore,tedsStore].forEach( store => {
store.items.push(toothpaste); // add toothpaste to store
toothpaste.stores.push(store); // add store to toothpaste
});
因为这不仅意味着我们将toothpaste
添加到每个商店中的"items"
数组,而且还将每个"store"
添加到toothpaste
项目的"stores"
数组中.这样做是为了可以从任一方向查询关系.如果只希望来自商店的商品"并且从不来自项目的商店",那么您根本不需要将关系数据存储在项目"条目上.
清单2-使用虚拟物品和中介收藏
这本质上是经典的多对多"关系.除了直接定义两个集合之间的关系之外,还有另一个集合(表)存储有关哪个项目与哪个商店相关的详细信息.
完整列表:
const { Schema } = mongoose = require('mongoose');
mongoose.Promise = global.Promise;
mongoose.set('debug',true);
mongoose.set('useFindAndModify', false);
mongoose.set('useCreateIndex', true);
const uri = 'mongodb://localhost:27017/manydemo',
options = { useNewUrlParser: true };
const itemSchema = new Schema({
name: String,
},{
toJSON: { virtuals: true }
});
itemSchema.virtual('stores', {
ref: 'StoreItem',
localField: '_id',
foreignField: 'itemId'
});
const storeSchema = new Schema({
name: String,
},{
toJSON: { virtuals: true }
});
storeSchema.virtual('items', {
ref: 'StoreItem',
localField: '_id',
foreignField: 'storeId'
});
const storeItemSchema = new Schema({
storeId: { type: Schema.Types.ObjectId, ref: 'Store', required: true },
itemId: { type: Schema.Types.ObjectId, ref: 'Item', required: true }
});
const Item = mongoose.model('Item', itemSchema);
const Store = mongoose.model('Store', storeSchema);
const StoreItem = mongoose.model('StoreItem', storeItemSchema);
const log = data => console.log(JSON.stringify(data,undefined,2));
(async function() {
try {
const conn = await mongoose.connect(uri,options);
// Clean data
await Promise.all(
Object.entries(conn.models).map(([k,m]) => m.deleteMany() )
);
// Create some instances
let [toothpaste,brush] = await Item.insertMany(
['toothpaste','brush'].map( name => ({ name }) )
);
let [billsStore,tedsStore] = await Store.insertMany(
['Bills','Teds'].map( name => ({ name }) )
);
// Add toothpaste to both stores
for( let store of [billsStore,tedsStore] ) {
await StoreItem.update(
{ storeId: store._id, itemId: toothpaste._id },
{ },
{ 'upsert': true }
);
}
// Add brush to billsStore
await StoreItem.update(
{ storeId: billsStore._id, itemId: brush._id },
{},
{ 'upsert': true }
);
// Show stores
let stores = await Store.find().populate({
path: 'items',
populate: { path: 'itemId' }
});
log(stores);
// Show Items
let items = await Item.find().populate({
path: 'stores',
populate: { path: 'storeId' }
});
log(items);
} catch(e) {
console.error(e);
} finally {
mongoose.disconnect();
}
})();
这些关系现在在自己的集合中,因此对于项目",数据现在以不同的方式出现:
{
"_id" : ObjectId("59ab996166d5cc0e0d164d74"),
"__v" : 0,
"name" : "toothpaste"
}
{
"_id" : ObjectId("59ab996166d5cc0e0d164d75"),
"__v" : 0,
"name" : "brush"
}
和商店":
{
"_id" : ObjectId("59ab996166d5cc0e0d164d76"),
"__v" : 0,
"name" : "Bills"
}
{
"_id" : ObjectId("59ab996166d5cc0e0d164d77"),
"__v" : 0,
"name" : "Teds"
}
现在针对映射关系的商店物品":
{
"_id" : ObjectId("59ab996179e41cc54405b72b"),
"itemId" : ObjectId("59ab996166d5cc0e0d164d74"),
"storeId" : ObjectId("59ab996166d5cc0e0d164d76"),
"__v" : 0
}
{
"_id" : ObjectId("59ab996179e41cc54405b72d"),
"itemId" : ObjectId("59ab996166d5cc0e0d164d74"),
"storeId" : ObjectId("59ab996166d5cc0e0d164d77"),
"__v" : 0
}
{
"_id" : ObjectId("59ab996179e41cc54405b72f"),
"itemId" : ObjectId("59ab996166d5cc0e0d164d75"),
"storeId" : ObjectId("59ab996166d5cc0e0d164d76"),
"__v" : 0
}
具有完整的输出,例如:
Mongoose: items.deleteMany({}, {})
Mongoose: stores.deleteMany({}, {})
Mongoose: storeitems.deleteMany({}, {})
Mongoose: items.insertMany([ { __v: 0, name: 'toothpaste', _id: 59ab996166d5cc0e0d164d74 }, { __v: 0, name: 'brush', _id: 59ab996166d5cc0e0d164d75 } ])
Mongoose: stores.insertMany([ { __v: 0, name: 'Bills', _id: 59ab996166d5cc0e0d164d76 }, { __v: 0, name: 'Teds', _id: 59ab996166d5cc0e0d164d77 } ])
Mongoose: storeitems.update({ itemId: ObjectId("59ab996166d5cc0e0d164d74"), storeId: ObjectId("59ab996166d5cc0e0d164d76") }, { '$setOnInsert': { __v: 0 } }, { upsert: true })
Mongoose: storeitems.update({ itemId: ObjectId("59ab996166d5cc0e0d164d74"), storeId: ObjectId("59ab996166d5cc0e0d164d77") }, { '$setOnInsert': { __v: 0 } }, { upsert: true })
Mongoose: storeitems.update({ itemId: ObjectId("59ab996166d5cc0e0d164d75"), storeId: ObjectId("59ab996166d5cc0e0d164d76") }, { '$setOnInsert': { __v: 0 } }, { upsert: true })
Mongoose: stores.find({}, { fields: {} })
Mongoose: storeitems.find({ storeId: { '$in': [ ObjectId("59ab996166d5cc0e0d164d76"), ObjectId("59ab996166d5cc0e0d164d77") ] } }, { fields: {} })
Mongoose: items.find({ _id: { '$in': [ ObjectId("59ab996166d5cc0e0d164d74"), ObjectId("59ab996166d5cc0e0d164d75") ] } }, { fields: {} })
[
{
"_id": "59ab996166d5cc0e0d164d76",
"__v": 0,
"name": "Bills",
"items": [
{
"_id": "59ab996179e41cc54405b72b",
"itemId": {
"_id": "59ab996166d5cc0e0d164d74",
"__v": 0,
"name": "toothpaste",
"stores": null,
"id": "59ab996166d5cc0e0d164d74"
},
"storeId": "59ab996166d5cc0e0d164d76",
"__v": 0
},
{
"_id": "59ab996179e41cc54405b72f",
"itemId": {
"_id": "59ab996166d5cc0e0d164d75",
"__v": 0,
"name": "brush",
"stores": null,
"id": "59ab996166d5cc0e0d164d75"
},
"storeId": "59ab996166d5cc0e0d164d76",
"__v": 0
}
],
"id": "59ab996166d5cc0e0d164d76"
},
{
"_id": "59ab996166d5cc0e0d164d77",
"__v": 0,
"name": "Teds",
"items": [
{
"_id": "59ab996179e41cc54405b72d",
"itemId": {
"_id": "59ab996166d5cc0e0d164d74",
"__v": 0,
"name": "toothpaste",
"stores": null,
"id": "59ab996166d5cc0e0d164d74"
},
"storeId": "59ab996166d5cc0e0d164d77",
"__v": 0
}
],
"id": "59ab996166d5cc0e0d164d77"
}
]
Mongoose: items.find({}, { fields: {} })
Mongoose: storeitems.find({ itemId: { '$in': [ ObjectId("59ab996166d5cc0e0d164d74"), ObjectId("59ab996166d5cc0e0d164d75") ] } }, { fields: {} })
Mongoose: stores.find({ _id: { '$in': [ ObjectId("59ab996166d5cc0e0d164d76"), ObjectId("59ab996166d5cc0e0d164d77") ] } }, { fields: {} })
[
{
"_id": "59ab996166d5cc0e0d164d74",
"__v": 0,
"name": "toothpaste",
"stores": [
{
"_id": "59ab996179e41cc54405b72b",
"itemId": "59ab996166d5cc0e0d164d74",
"storeId": {
"_id": "59ab996166d5cc0e0d164d76",
"__v": 0,
"name": "Bills",
"items": null,
"id": "59ab996166d5cc0e0d164d76"
},
"__v": 0
},
{
"_id": "59ab996179e41cc54405b72d",
"itemId": "59ab996166d5cc0e0d164d74",
"storeId": {
"_id": "59ab996166d5cc0e0d164d77",
"__v": 0,
"name": "Teds",
"items": null,
"id": "59ab996166d5cc0e0d164d77"
},
"__v": 0
}
],
"id": "59ab996166d5cc0e0d164d74"
},
{
"_id": "59ab996166d5cc0e0d164d75",
"__v": 0,
"name": "brush",
"stores": [
{
"_id": "59ab996179e41cc54405b72f",
"itemId": "59ab996166d5cc0e0d164d75",
"storeId": {
"_id": "59ab996166d5cc0e0d164d76",
"__v": 0,
"name": "Bills",
"items": null,
"id": "59ab996166d5cc0e0d164d76"
},
"__v": 0
}
],
"id": "59ab996166d5cc0e0d164d75"
}
]
由于现在已将关系映射到一个单独的集合中,因此这里有一些更改.值得注意的是,我们要在集合上定义一个虚拟"字段,该字段不再具有固定的项目数组.因此,您需要添加一个,如图所示:
const itemSchema = new Schema({
name: String,
},{
toJSON: { virtuals: true }
});
itemSchema.virtual('stores', {
ref: 'StoreItem',
localField: '_id',
foreignField: 'itemId'
});
您为虚拟字段分配了localField
和foreignField
映射,以便随后的.populate()
调用知道要使用什么.
中介集合具有相当标准的定义:
const storeItemSchema = new Schema({
storeId: { type: Schema.Types.ObjectId, ref: 'Store', required: true },
itemId: { type: Schema.Types.ObjectId, ref: 'Item', required: true }
});
我们不是将新项目推"到数组上,而是将它们添加到此新集合中.一种合理的方法是仅在此组合不存在时才使用"upserts"创建一个新条目:
// Add toothpaste to both stores
for( let store of [billsStore,tedsStore] ) {
await StoreItem.update(
{ storeId: store._id, itemId: toothpaste._id },
{ },
{ 'upsert': true }
);
}
这是一个非常简单的方法,它仅使用查询中提供的两个键(未找到一个键)来创建一个新文档,或者在匹配时实质上尝试更新同一文档,在这种情况下为"nothing".因此,现有比赛最终以无操作"告终,这是需要做的事情.或者,您可以简单地.insertOne()
忽略重复的键错误.随便你怎么看.
实际上,查询此相关"数据的方法有所不同.因为涉及到另一个集合,所以我们以某种方式调用.populate()
,它认为它也需要查找"其他检索到的属性上的关系.所以您会有这样的呼叫:
// Show stores
let stores = await Store.find().populate({
path: 'items',
populate: { path: 'itemId' }
});
log(stores);
清单3-使用现代功能在服务器上完成
因此,根据采用哪种方法,是使用数组或中介集合来存储关系数据,以替代文档中的增长数组",那么您应该注意到的明显的是,.populate()
调用实际上是在对MongoDB进行其他查询,并在单独的请求中通过网络提取这些文档.
小剂量服用可能看起来很好,但是随着事情的发展,尤其是随着需求量的增加,这从来都不是一件好事.另外,您可能还需要应用其他条件,这意味着您不需要从服务器中提取所有文档,而是希望在返回结果之前匹配那些关系"中的数据.
这就是为什么现代MongoDB版本包含 $lookup
的原因实际上是联接"服务器本身上的数据.到目前为止,您应该已经查看了这些API调用产生的所有输出,如mongoose.set('debug',true)
所示.
因此,这次而不是生成多个查询,而是使它成为一条聚合语句以在服务器上加入",并在一个请求中返回结果:
// Show Stores
let stores = await Store.aggregate([
{ '$lookup': {
'from': StoreItem.collection.name,
'let': { 'id': '$_id' },
'pipeline': [
{ '$match': {
'$expr': { '$eq': [ '$$id', '$storeId' ] }
}},
{ '$lookup': {
'from': Item.collection.name,
'let': { 'itemId': '$itemId' },
'pipeline': [
{ '$match': {
'$expr': { '$eq': [ '$_id', '$$itemId' ] }
}}
],
'as': 'items'
}},
{ '$unwind': '$items' },
{ '$replaceRoot': { 'newRoot': '$items' } }
],
'as': 'items'
}}
])
log(stores);
尽管编码时间更长,但实际上即使在这里进行非常琐碎的操作,其效率实际上也要好得多.这当然可以扩展.
遵循与以前相同的中介"模型(仅举例来说,因为可以通过任何一种方式完成),我们都有完整的清单:
const { Schema } = mongoose = require('mongoose');
const uri = 'mongodb://localhost:27017/manydemo',
options = { useNewUrlParser: true };
mongoose.Promise = global.Promise;
mongoose.set('debug', true);
mongoose.set('useFindAndModify', false);
mongoose.set('useCreateIndex', true);
const itemSchema = new Schema({
name: String
}, {
toJSON: { virtuals: true }
});
itemSchema.virtual('stores', {
ref: 'StoreItem',
localField: '_id',
foreignField: 'itemId'
});
const storeSchema = new Schema({
name: String
}, {
toJSON: { virtuals: true }
});
storeSchema.virtual('items', {
ref: 'StoreItem',
localField: '_id',
foreignField: 'storeId'
});
const storeItemSchema = new Schema({
storeId: { type: Schema.Types.ObjectId, ref: 'Store', required: true },
itemId: { type: Schema.Types.ObjectId, ref: 'Item', required: true }
});
const Item = mongoose.model('Item', itemSchema);
const Store = mongoose.model('Store', storeSchema);
const StoreItem = mongoose.model('StoreItem', storeItemSchema);
const log = data => console.log(JSON.stringify(data, undefined, 2));
(async function() {
try {
const conn = await mongoose.connect(uri, options);
// Clean data
await Promise.all(
Object.entries(conn.models).map(([k,m]) => m.deleteMany())
);
// Create some instances
let [toothpaste, brush] = await Item.insertMany(
['toothpaste', 'brush'].map(name => ({ name }) )
);
let [billsStore, tedsStore] = await Store.insertMany(
['Bills', 'Teds'].map( name => ({ name }) )
);
// Add toothpaste to both stores
for ( let { _id: storeId } of [billsStore, tedsStore] ) {
await StoreItem.updateOne(
{ storeId, itemId: toothpaste._id },
{ },
{ 'upsert': true }
);
}
// Add brush to billsStore
await StoreItem.updateOne(
{ storeId: billsStore._id, itemId: brush._id },
{ },
{ 'upsert': true }
);
// Show Stores
let stores = await Store.aggregate([
{ '$lookup': {
'from': StoreItem.collection.name,
'let': { 'id': '$_id' },
'pipeline': [
{ '$match': {
'$expr': { '$eq': [ '$$id', '$storeId' ] }
}},
{ '$lookup': {
'from': Item.collection.name,
'let': { 'itemId': '$itemId' },
'pipeline': [
{ '$match': {
'$expr': { '$eq': [ '$_id', '$$itemId' ] }
}}
],
'as': 'items'
}},
{ '$unwind': '$items' },
{ '$replaceRoot': { 'newRoot': '$items' } }
],
'as': 'items'
}}
])
log(stores);
// Show Items
let items = await Item.aggregate([
{ '$lookup': {
'from': StoreItem.collection.name,
'let': { 'id': '$_id' },
'pipeline': [
{ '$match': {
'$expr': { '$eq': [ '$$id', '$itemId' ] }
}},
{ '$lookup': {
'from': Store.collection.name,
'let': { 'storeId': '$storeId' },
'pipeline': [
{ '$match': {
'$expr': { '$eq': [ '$_id', '$$storeId' ] }
}}
],
'as': 'stores',
}},
{ '$unwind': '$stores' },
{ '$replaceRoot': { 'newRoot': '$stores' } }
],
'as': 'stores'
}}
]);
log(items);
} catch(e) {
console.error(e);
} finally {
mongoose.disconnect();
}
})()
输出:
Mongoose: stores.aggregate([ { '$lookup': { from: 'storeitems', let: { id: '$_id' }, pipeline: [ { '$match': { '$expr': { '$eq': [ '$$id', '$storeId' ] } } }, { '$lookup': { from: 'items', let: { itemId: '$itemId' }, pipeline: [ { '$match': { '$expr': { '$eq': [ '$_id', '$$itemId' ] } } } ], as: 'items' } }, { '$unwind': '$items' }, { '$replaceRoot': { newRoot: '$items' } } ], as: 'items' } } ], {})
[
{
"_id": "5ca7210717dadc69652b37da",
"name": "Bills",
"__v": 0,
"items": [
{
"_id": "5ca7210717dadc69652b37d8",
"name": "toothpaste",
"__v": 0
},
{
"_id": "5ca7210717dadc69652b37d9",
"name": "brush",
"__v": 0
}
]
},
{
"_id": "5ca7210717dadc69652b37db",
"name": "Teds",
"__v": 0,
"items": [
{
"_id": "5ca7210717dadc69652b37d8",
"name": "toothpaste",
"__v": 0
}
]
}
]
Mongoose: items.aggregate([ { '$lookup': { from: 'storeitems', let: { id: '$_id' }, pipeline: [ { '$match': { '$expr': { '$eq': [ '$$id', '$itemId' ] } } }, { '$lookup': { from: 'stores', let: { storeId: '$storeId' }, pipeline: [ { '$match': { '$expr': { '$eq': [ '$_id', '$$storeId' ] } } } ], as: 'stores' } }, { '$unwind': '$stores' }, { '$replaceRoot': { newRoot: '$stores' } } ], as: 'stores' } } ], {})
[
{
"_id": "5ca7210717dadc69652b37d8",
"name": "toothpaste",
"__v": 0,
"stores": [
{
"_id": "5ca7210717dadc69652b37da",
"name": "Bills",
"__v": 0
},
{
"_id": "5ca7210717dadc69652b37db",
"name": "Teds",
"__v": 0
}
]
},
{
"_id": "5ca7210717dadc69652b37d9",
"name": "brush",
"__v": 0,
"stores": [
{
"_id": "5ca7210717dadc69652b37da",
"name": "Bills",
"__v": 0
}
]
}
]
显而易见的是,最后发出的查询的显着减少是返回数据的联接"形式.这意味着由于消除了所有网络开销,从而降低了延迟并提高了应用程序的响应速度.
最后的笔记
通常,这是您处理多对多"关系的方法,基本上可以归结为以下任何一种:
-
每侧文档中的数组都保留着对相关项目的引用.
-
存储一个中间集合,并将其用作检索其他数据的查找参考.
在所有情况下,如果您希望事情在两个方向"上都能起作用,则完全由您决定.当然, $lookup
甚至适用的虚拟"都意味着您不必总是存储在每个来源上,因为您可以在一个地方引用"并通过应用这些方法来使用该信息.
另一种情况当然是嵌入",这是一个完全不同的游戏,而诸如MongoDB之类的面向文档的数据库的真正意义所在.因此,该概念当然不是从另一个集合中获取",而是嵌入"数据.
这意味着不仅ObjectId
值指向其他项目,而且实际上将完整数据存储在每个文档的数组中.当然存在大小"的问题,当然还有在多个位置更新数据的问题.通常,这是一个单个请求和一个简单请求的折衷方案,它们不需要去查找其他集合中的数据,因为它已经存在"了. /p>
关于引用和嵌入的主题周围有很多资料.一旦这样的摘要源为猫鼬填充与对象嵌套,甚至是非常普通的 The The problem you presently have is that you saved the reference in one model but you did not save it in the other. There is no "automatic referential integrity" in MongoDB, and such concept of "relations" are really a "manual" affair, and in fact the case with Correct handling of "many to many" comes down to three options: Following your current design, the parts you are missing is storing the referenced on "both" the related items. For a listing to demonstrate: This creates the "items" collection: And the "stores" collection: And produces overall output such as: The key points being that you actually add the reference data to each document in each collection where a relationship exists. The "arrays" present are used here to store those references and "lookup" the results from the related collection and replace them with the object data that was stored there. Pay attention to parts like: Because that means not only are we adding the This is essentially the classic "many to many" relation. Where instead of directly defining relationships between the two collections, there is another collection ( table ) that stores the details about which item is related to which store. As a full listing: The relations are now in their own collection, so the data now appears differently, for "items": And "stores": And now for "storeitems" which maps the relations: With full output like: Since the relations are now mapped in a separate collection there are a couple of changes here. Notably we want to define a "virtual" field on the collection that no longer has a fixed array of items. So you add one as is shown: You assign the virtual field with it's The intermediary collection has a fairly standard definition: And instead of "pushing" new items onto arrays, we instead add them to this new collection. A reasonable method for this is using "upserts" to create a new entry only when this combination does not exist: It's a pretty simple method that merely creates a new document with the two keys supplied in the query where one was not found, or essentially tries to update the same document when matched, and with "nothing" in this case. So existing matches just end up as a "no-op", which is the desired thing to do. Alternately you could simply Actually querying this "related" data works a little differently again. Because there is another collection involved, we call
So depending on which approach taken, being using arrays or an intermediary collection to store the relation data in as an alternative to "growing arrays" within the documents, then the obvious thing you should be noting is that the This might appear all well and fine in small doses, however as things scale up and especially over volumes of requests, this is never a good thing. Additionally there might well be other conditions you want to apply that means you don't need to pull all the documents from the server, and would rather match data from those "relations" before you returned results. This is why modern MongoDB releases include So instead of producing multiple queries, this time we make it one aggregation statement to "join" on the server, and return the results in one request: Which whilst longer in coding, is actually far superior in efficiency even for the very trivial action right here. This of course scales considerably. Following the same "intermediary" model as before ( and just for example, because it could be done either way ) we have a full listing: And the output: What should be obvious is the significant reduction in the queries issued on the end to return the "joined" form of the data. This means lower latency and more responsive applications as a result of removing all the network overhead. Those a are generally your approaches to dealing with "many to many" relations, which essentially comes down to either: Keeping arrays in each document on either side holding the references to the related items. Storing an intermediary collection and using that as a lookup reference to retrieving the other data. In all cases it is up to you to actually store those references if you expect things to work on "both directions". Of course The other case is of course "embedding", which is an entirely different game and what document oriented databases such as MongoDB are really all about. Therefore instead of "fetching from another collection" the concept is of course to "embed" the data. This means not just the There is plenty of material around on the subject of referencing vs embedding. Once such summary source is Mongoose populate vs object nesting or even the very general MongoDB relationships: embed or reference? and many many others. You should spend some time thinking about the concepts and how this applies to your application in general. And note that you are not actually using an RDBMS here, so you might as well use the correct features that you are meant to exploit, rather than simply making one act like the other. 这篇关于猫鼬多对多的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!store
is saved with the items
array containing 1 item
. The item
though, doesn't have stores
. What am I missing? How to automatically update the many-to-many relationships in MongoDB/Mongoose? I was used to Rails and everything was done automatically..populate()
is actually a whole bunch of additional queries in order to retrieve the referenced information. No "magic" here.Listing 1 - Keep arrays on Both documents
const { Schema } = mongoose = require('mongoose');
mongoose.Promise = global.Promise;
mongoose.set('debug',true);
mongoose.set('useFindAndModify', false);
mongoose.set('useCreateIndex', true);
const uri = 'mongodb://localhost:27017/manydemo',
options = { useNewUrlParser: true };
const itemSchema = new Schema({
name: String,
stores: [{ type: Schema.Types.ObjectId, ref: 'Store' }]
});
const storeSchema = new Schema({
name: String,
items: [{ type: Schema.Types.ObjectId, ref: 'Item' }]
});
const Item = mongoose.model('Item', itemSchema);
const Store = mongoose.model('Store', storeSchema);
const log = data => console.log(JSON.stringify(data,undefined,2))
(async function() {
try {
const conn = await mongoose.connect(uri,options);
// Clean data
await Promise.all(
Object.entries(conn.models).map(([k,m]) => m.deleteMany() )
);
// Create some instances
let [toothpaste,brush] = ['toothpaste','brush'].map(
name => new Item({ name })
);
let [billsStore,tedsStore] = ['Bills','Teds'].map(
name => new Store({ name })
);
// Add items to stores
[billsStore,tedsStore].forEach( store => {
store.items.push(toothpaste); // add toothpaste to store
toothpaste.stores.push(store); // add store to toothpaste
});
// Brush is only in billsStore
billsStore.items.push(brush);
brush.stores.push(billsStore);
// Save everything
await Promise.all(
[toothpaste,brush,billsStore,tedsStore].map( m => m.save() )
);
// Show stores
let stores = await Store.find().populate('items','-stores');
log(stores);
// Show items
let items = await Item.find().populate('stores','-items');
log(items);
} catch(e) {
console.error(e);
} finally {
mongoose.disconnect();
}
})();
{
"_id" : ObjectId("59ab96d9c079220dd8eec428"),
"name" : "toothpaste",
"stores" : [
ObjectId("59ab96d9c079220dd8eec42a"),
ObjectId("59ab96d9c079220dd8eec42b")
],
"__v" : 0
}
{
"_id" : ObjectId("59ab96d9c079220dd8eec429"),
"name" : "brush",
"stores" : [
ObjectId("59ab96d9c079220dd8eec42a")
],
"__v" : 0
}
{
"_id" : ObjectId("59ab96d9c079220dd8eec42a"),
"name" : "Bills",
"items" : [
ObjectId("59ab96d9c079220dd8eec428"),
ObjectId("59ab96d9c079220dd8eec429")
],
"__v" : 0
}
{
"_id" : ObjectId("59ab96d9c079220dd8eec42b"),
"name" : "Teds",
"items" : [
ObjectId("59ab96d9c079220dd8eec428")
],
"__v" : 0
}
Mongoose: items.deleteMany({}, {})
Mongoose: stores.deleteMany({}, {})
Mongoose: items.insertOne({ name: 'toothpaste', _id: ObjectId("59ab96d9c079220dd8eec428"), stores: [ ObjectId("59ab96d9c079220dd8eec42a"), ObjectId("59ab96d9c079220dd8eec42b") ], __v: 0 })
Mongoose: items.insertOne({ name: 'brush', _id: ObjectId("59ab96d9c079220dd8eec429"), stores: [ ObjectId("59ab96d9c079220dd8eec42a") ], __v: 0 })
Mongoose: stores.insertOne({ name: 'Bills', _id: ObjectId("59ab96d9c079220dd8eec42a"), items: [ ObjectId("59ab96d9c079220dd8eec428"), ObjectId("59ab96d9c079220dd8eec429") ], __v: 0 })
Mongoose: stores.insertOne({ name: 'Teds', _id: ObjectId("59ab96d9c079220dd8eec42b"), items: [ ObjectId("59ab96d9c079220dd8eec428") ], __v: 0 })
Mongoose: stores.find({}, { fields: {} })
Mongoose: items.find({ _id: { '$in': [ ObjectId("59ab96d9c079220dd8eec428"), ObjectId("59ab96d9c079220dd8eec429") ] } }, { fields: { stores: 0 } })
[
{
"_id": "59ab96d9c079220dd8eec42a",
"name": "Bills",
"__v": 0,
"items": [
{
"_id": "59ab96d9c079220dd8eec428",
"name": "toothpaste",
"__v": 0
},
{
"_id": "59ab96d9c079220dd8eec429",
"name": "brush",
"__v": 0
}
]
},
{
"_id": "59ab96d9c079220dd8eec42b",
"name": "Teds",
"__v": 0,
"items": [
{
"_id": "59ab96d9c079220dd8eec428",
"name": "toothpaste",
"__v": 0
}
]
}
]
Mongoose: items.find({}, { fields: {} })
Mongoose: stores.find({ _id: { '$in': [ ObjectId("59ab96d9c079220dd8eec42a"), ObjectId("59ab96d9c079220dd8eec42b") ] } }, { fields: { items: 0 } })
[
{
"_id": "59ab96d9c079220dd8eec428",
"name": "toothpaste",
"__v": 0,
"stores": [
{
"_id": "59ab96d9c079220dd8eec42a",
"name": "Bills",
"__v": 0
},
{
"_id": "59ab96d9c079220dd8eec42b",
"name": "Teds",
"__v": 0
}
]
},
{
"_id": "59ab96d9c079220dd8eec429",
"name": "brush",
"__v": 0,
"stores": [
{
"_id": "59ab96d9c079220dd8eec42a",
"name": "Bills",
"__v": 0
}
]
}
]
// Add items to stores
[billsStore,tedsStore].forEach( store => {
store.items.push(toothpaste); // add toothpaste to store
toothpaste.stores.push(store); // add store to toothpaste
});
toothpaste
to the "items"
array in each store, but we are also adding each "store"
to the "stores"
array of the toothpaste
item. This is done so the relationships can work being queried from either direction. If you only wanted "items from stores" and never "stores from items", then you would not need to store the relation data on the "item" entries at all.Listing 2 - Use Virtuals and an Intermediary Collection
const { Schema } = mongoose = require('mongoose');
mongoose.Promise = global.Promise;
mongoose.set('debug',true);
mongoose.set('useFindAndModify', false);
mongoose.set('useCreateIndex', true);
const uri = 'mongodb://localhost:27017/manydemo',
options = { useNewUrlParser: true };
const itemSchema = new Schema({
name: String,
},{
toJSON: { virtuals: true }
});
itemSchema.virtual('stores', {
ref: 'StoreItem',
localField: '_id',
foreignField: 'itemId'
});
const storeSchema = new Schema({
name: String,
},{
toJSON: { virtuals: true }
});
storeSchema.virtual('items', {
ref: 'StoreItem',
localField: '_id',
foreignField: 'storeId'
});
const storeItemSchema = new Schema({
storeId: { type: Schema.Types.ObjectId, ref: 'Store', required: true },
itemId: { type: Schema.Types.ObjectId, ref: 'Item', required: true }
});
const Item = mongoose.model('Item', itemSchema);
const Store = mongoose.model('Store', storeSchema);
const StoreItem = mongoose.model('StoreItem', storeItemSchema);
const log = data => console.log(JSON.stringify(data,undefined,2));
(async function() {
try {
const conn = await mongoose.connect(uri,options);
// Clean data
await Promise.all(
Object.entries(conn.models).map(([k,m]) => m.deleteMany() )
);
// Create some instances
let [toothpaste,brush] = await Item.insertMany(
['toothpaste','brush'].map( name => ({ name }) )
);
let [billsStore,tedsStore] = await Store.insertMany(
['Bills','Teds'].map( name => ({ name }) )
);
// Add toothpaste to both stores
for( let store of [billsStore,tedsStore] ) {
await StoreItem.update(
{ storeId: store._id, itemId: toothpaste._id },
{ },
{ 'upsert': true }
);
}
// Add brush to billsStore
await StoreItem.update(
{ storeId: billsStore._id, itemId: brush._id },
{},
{ 'upsert': true }
);
// Show stores
let stores = await Store.find().populate({
path: 'items',
populate: { path: 'itemId' }
});
log(stores);
// Show Items
let items = await Item.find().populate({
path: 'stores',
populate: { path: 'storeId' }
});
log(items);
} catch(e) {
console.error(e);
} finally {
mongoose.disconnect();
}
})();
{
"_id" : ObjectId("59ab996166d5cc0e0d164d74"),
"__v" : 0,
"name" : "toothpaste"
}
{
"_id" : ObjectId("59ab996166d5cc0e0d164d75"),
"__v" : 0,
"name" : "brush"
}
{
"_id" : ObjectId("59ab996166d5cc0e0d164d76"),
"__v" : 0,
"name" : "Bills"
}
{
"_id" : ObjectId("59ab996166d5cc0e0d164d77"),
"__v" : 0,
"name" : "Teds"
}
{
"_id" : ObjectId("59ab996179e41cc54405b72b"),
"itemId" : ObjectId("59ab996166d5cc0e0d164d74"),
"storeId" : ObjectId("59ab996166d5cc0e0d164d76"),
"__v" : 0
}
{
"_id" : ObjectId("59ab996179e41cc54405b72d"),
"itemId" : ObjectId("59ab996166d5cc0e0d164d74"),
"storeId" : ObjectId("59ab996166d5cc0e0d164d77"),
"__v" : 0
}
{
"_id" : ObjectId("59ab996179e41cc54405b72f"),
"itemId" : ObjectId("59ab996166d5cc0e0d164d75"),
"storeId" : ObjectId("59ab996166d5cc0e0d164d76"),
"__v" : 0
}
Mongoose: items.deleteMany({}, {})
Mongoose: stores.deleteMany({}, {})
Mongoose: storeitems.deleteMany({}, {})
Mongoose: items.insertMany([ { __v: 0, name: 'toothpaste', _id: 59ab996166d5cc0e0d164d74 }, { __v: 0, name: 'brush', _id: 59ab996166d5cc0e0d164d75 } ])
Mongoose: stores.insertMany([ { __v: 0, name: 'Bills', _id: 59ab996166d5cc0e0d164d76 }, { __v: 0, name: 'Teds', _id: 59ab996166d5cc0e0d164d77 } ])
Mongoose: storeitems.update({ itemId: ObjectId("59ab996166d5cc0e0d164d74"), storeId: ObjectId("59ab996166d5cc0e0d164d76") }, { '$setOnInsert': { __v: 0 } }, { upsert: true })
Mongoose: storeitems.update({ itemId: ObjectId("59ab996166d5cc0e0d164d74"), storeId: ObjectId("59ab996166d5cc0e0d164d77") }, { '$setOnInsert': { __v: 0 } }, { upsert: true })
Mongoose: storeitems.update({ itemId: ObjectId("59ab996166d5cc0e0d164d75"), storeId: ObjectId("59ab996166d5cc0e0d164d76") }, { '$setOnInsert': { __v: 0 } }, { upsert: true })
Mongoose: stores.find({}, { fields: {} })
Mongoose: storeitems.find({ storeId: { '$in': [ ObjectId("59ab996166d5cc0e0d164d76"), ObjectId("59ab996166d5cc0e0d164d77") ] } }, { fields: {} })
Mongoose: items.find({ _id: { '$in': [ ObjectId("59ab996166d5cc0e0d164d74"), ObjectId("59ab996166d5cc0e0d164d75") ] } }, { fields: {} })
[
{
"_id": "59ab996166d5cc0e0d164d76",
"__v": 0,
"name": "Bills",
"items": [
{
"_id": "59ab996179e41cc54405b72b",
"itemId": {
"_id": "59ab996166d5cc0e0d164d74",
"__v": 0,
"name": "toothpaste",
"stores": null,
"id": "59ab996166d5cc0e0d164d74"
},
"storeId": "59ab996166d5cc0e0d164d76",
"__v": 0
},
{
"_id": "59ab996179e41cc54405b72f",
"itemId": {
"_id": "59ab996166d5cc0e0d164d75",
"__v": 0,
"name": "brush",
"stores": null,
"id": "59ab996166d5cc0e0d164d75"
},
"storeId": "59ab996166d5cc0e0d164d76",
"__v": 0
}
],
"id": "59ab996166d5cc0e0d164d76"
},
{
"_id": "59ab996166d5cc0e0d164d77",
"__v": 0,
"name": "Teds",
"items": [
{
"_id": "59ab996179e41cc54405b72d",
"itemId": {
"_id": "59ab996166d5cc0e0d164d74",
"__v": 0,
"name": "toothpaste",
"stores": null,
"id": "59ab996166d5cc0e0d164d74"
},
"storeId": "59ab996166d5cc0e0d164d77",
"__v": 0
}
],
"id": "59ab996166d5cc0e0d164d77"
}
]
Mongoose: items.find({}, { fields: {} })
Mongoose: storeitems.find({ itemId: { '$in': [ ObjectId("59ab996166d5cc0e0d164d74"), ObjectId("59ab996166d5cc0e0d164d75") ] } }, { fields: {} })
Mongoose: stores.find({ _id: { '$in': [ ObjectId("59ab996166d5cc0e0d164d76"), ObjectId("59ab996166d5cc0e0d164d77") ] } }, { fields: {} })
[
{
"_id": "59ab996166d5cc0e0d164d74",
"__v": 0,
"name": "toothpaste",
"stores": [
{
"_id": "59ab996179e41cc54405b72b",
"itemId": "59ab996166d5cc0e0d164d74",
"storeId": {
"_id": "59ab996166d5cc0e0d164d76",
"__v": 0,
"name": "Bills",
"items": null,
"id": "59ab996166d5cc0e0d164d76"
},
"__v": 0
},
{
"_id": "59ab996179e41cc54405b72d",
"itemId": "59ab996166d5cc0e0d164d74",
"storeId": {
"_id": "59ab996166d5cc0e0d164d77",
"__v": 0,
"name": "Teds",
"items": null,
"id": "59ab996166d5cc0e0d164d77"
},
"__v": 0
}
],
"id": "59ab996166d5cc0e0d164d74"
},
{
"_id": "59ab996166d5cc0e0d164d75",
"__v": 0,
"name": "brush",
"stores": [
{
"_id": "59ab996179e41cc54405b72f",
"itemId": "59ab996166d5cc0e0d164d75",
"storeId": {
"_id": "59ab996166d5cc0e0d164d76",
"__v": 0,
"name": "Bills",
"items": null,
"id": "59ab996166d5cc0e0d164d76"
},
"__v": 0
}
],
"id": "59ab996166d5cc0e0d164d75"
}
]
const itemSchema = new Schema({
name: String,
},{
toJSON: { virtuals: true }
});
itemSchema.virtual('stores', {
ref: 'StoreItem',
localField: '_id',
foreignField: 'itemId'
});
localField
and foreignField
mappings so the subsequent .populate()
call knows what to use.const storeItemSchema = new Schema({
storeId: { type: Schema.Types.ObjectId, ref: 'Store', required: true },
itemId: { type: Schema.Types.ObjectId, ref: 'Item', required: true }
});
// Add toothpaste to both stores
for( let store of [billsStore,tedsStore] ) {
await StoreItem.update(
{ storeId: store._id, itemId: toothpaste._id },
{ },
{ 'upsert': true }
);
}
.insertOne()
an ignore duplicate key errors. Whatever takes your fancy..populate()
in a way that considers it needs to "lookup" the relation on other retrieved property as well. So you have calls like this: // Show stores
let stores = await Store.find().populate({
path: 'items',
populate: { path: 'itemId' }
});
log(stores);
Listing 3 - Use Modern Features to do it on the server
.populate()
calls used are actually making additional queries to MongoDB and pulling those documents over the network in separate requests.$lookup
which actually "joins" the data on the server itself. By now you should have been looking at all the output those API calls produce as shown by mongoose.set('debug',true)
.// Show Stores
let stores = await Store.aggregate([
{ '$lookup': {
'from': StoreItem.collection.name,
'let': { 'id': '$_id' },
'pipeline': [
{ '$match': {
'$expr': { '$eq': [ '$$id', '$storeId' ] }
}},
{ '$lookup': {
'from': Item.collection.name,
'let': { 'itemId': '$itemId' },
'pipeline': [
{ '$match': {
'$expr': { '$eq': [ '$_id', '$$itemId' ] }
}}
],
'as': 'items'
}},
{ '$unwind': '$items' },
{ '$replaceRoot': { 'newRoot': '$items' } }
],
'as': 'items'
}}
])
log(stores);
const { Schema } = mongoose = require('mongoose');
const uri = 'mongodb://localhost:27017/manydemo',
options = { useNewUrlParser: true };
mongoose.Promise = global.Promise;
mongoose.set('debug', true);
mongoose.set('useFindAndModify', false);
mongoose.set('useCreateIndex', true);
const itemSchema = new Schema({
name: String
}, {
toJSON: { virtuals: true }
});
itemSchema.virtual('stores', {
ref: 'StoreItem',
localField: '_id',
foreignField: 'itemId'
});
const storeSchema = new Schema({
name: String
}, {
toJSON: { virtuals: true }
});
storeSchema.virtual('items', {
ref: 'StoreItem',
localField: '_id',
foreignField: 'storeId'
});
const storeItemSchema = new Schema({
storeId: { type: Schema.Types.ObjectId, ref: 'Store', required: true },
itemId: { type: Schema.Types.ObjectId, ref: 'Item', required: true }
});
const Item = mongoose.model('Item', itemSchema);
const Store = mongoose.model('Store', storeSchema);
const StoreItem = mongoose.model('StoreItem', storeItemSchema);
const log = data => console.log(JSON.stringify(data, undefined, 2));
(async function() {
try {
const conn = await mongoose.connect(uri, options);
// Clean data
await Promise.all(
Object.entries(conn.models).map(([k,m]) => m.deleteMany())
);
// Create some instances
let [toothpaste, brush] = await Item.insertMany(
['toothpaste', 'brush'].map(name => ({ name }) )
);
let [billsStore, tedsStore] = await Store.insertMany(
['Bills', 'Teds'].map( name => ({ name }) )
);
// Add toothpaste to both stores
for ( let { _id: storeId } of [billsStore, tedsStore] ) {
await StoreItem.updateOne(
{ storeId, itemId: toothpaste._id },
{ },
{ 'upsert': true }
);
}
// Add brush to billsStore
await StoreItem.updateOne(
{ storeId: billsStore._id, itemId: brush._id },
{ },
{ 'upsert': true }
);
// Show Stores
let stores = await Store.aggregate([
{ '$lookup': {
'from': StoreItem.collection.name,
'let': { 'id': '$_id' },
'pipeline': [
{ '$match': {
'$expr': { '$eq': [ '$$id', '$storeId' ] }
}},
{ '$lookup': {
'from': Item.collection.name,
'let': { 'itemId': '$itemId' },
'pipeline': [
{ '$match': {
'$expr': { '$eq': [ '$_id', '$$itemId' ] }
}}
],
'as': 'items'
}},
{ '$unwind': '$items' },
{ '$replaceRoot': { 'newRoot': '$items' } }
],
'as': 'items'
}}
])
log(stores);
// Show Items
let items = await Item.aggregate([
{ '$lookup': {
'from': StoreItem.collection.name,
'let': { 'id': '$_id' },
'pipeline': [
{ '$match': {
'$expr': { '$eq': [ '$$id', '$itemId' ] }
}},
{ '$lookup': {
'from': Store.collection.name,
'let': { 'storeId': '$storeId' },
'pipeline': [
{ '$match': {
'$expr': { '$eq': [ '$_id', '$$storeId' ] }
}}
],
'as': 'stores',
}},
{ '$unwind': '$stores' },
{ '$replaceRoot': { 'newRoot': '$stores' } }
],
'as': 'stores'
}}
]);
log(items);
} catch(e) {
console.error(e);
} finally {
mongoose.disconnect();
}
})()
Mongoose: stores.aggregate([ { '$lookup': { from: 'storeitems', let: { id: '$_id' }, pipeline: [ { '$match': { '$expr': { '$eq': [ '$$id', '$storeId' ] } } }, { '$lookup': { from: 'items', let: { itemId: '$itemId' }, pipeline: [ { '$match': { '$expr': { '$eq': [ '$_id', '$$itemId' ] } } } ], as: 'items' } }, { '$unwind': '$items' }, { '$replaceRoot': { newRoot: '$items' } } ], as: 'items' } } ], {})
[
{
"_id": "5ca7210717dadc69652b37da",
"name": "Bills",
"__v": 0,
"items": [
{
"_id": "5ca7210717dadc69652b37d8",
"name": "toothpaste",
"__v": 0
},
{
"_id": "5ca7210717dadc69652b37d9",
"name": "brush",
"__v": 0
}
]
},
{
"_id": "5ca7210717dadc69652b37db",
"name": "Teds",
"__v": 0,
"items": [
{
"_id": "5ca7210717dadc69652b37d8",
"name": "toothpaste",
"__v": 0
}
]
}
]
Mongoose: items.aggregate([ { '$lookup': { from: 'storeitems', let: { id: '$_id' }, pipeline: [ { '$match': { '$expr': { '$eq': [ '$$id', '$itemId' ] } } }, { '$lookup': { from: 'stores', let: { storeId: '$storeId' }, pipeline: [ { '$match': { '$expr': { '$eq': [ '$_id', '$$storeId' ] } } } ], as: 'stores' } }, { '$unwind': '$stores' }, { '$replaceRoot': { newRoot: '$stores' } } ], as: 'stores' } } ], {})
[
{
"_id": "5ca7210717dadc69652b37d8",
"name": "toothpaste",
"__v": 0,
"stores": [
{
"_id": "5ca7210717dadc69652b37da",
"name": "Bills",
"__v": 0
},
{
"_id": "5ca7210717dadc69652b37db",
"name": "Teds",
"__v": 0
}
]
},
{
"_id": "5ca7210717dadc69652b37d9",
"name": "brush",
"__v": 0,
"stores": [
{
"_id": "5ca7210717dadc69652b37da",
"name": "Bills",
"__v": 0
}
]
}
]
Final Notes
$lookup
and even "virtuals" where that applies means that you don't always need to store on every source since you could then "reference" in just one place and use that information by applying those methods.ObjectId
values that point to the other items, but actually storing the full data within arrays in each document. There is of course an issue of "size" and of course issues with updating data in multiple places. This is generally the trade off for there being a single request and a simple request that does not need to go and find data in other collections because it's "already there".