Mongoose 多对多

2022-01-13 00:00:00 mongodb node.js nosql javascript 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 个 itemitems 数组一起保存.item 但是没有stores.我错过了什么?如何自动更新 MongoDB/Mongoose 中的多对多关系?我习惯了 Rails,一切都是自动完成的.

The 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.

推荐答案

您目前遇到的问题是您将引用保存在一个模型中,但您没有将其保存在另一个模型中.MongoDB中没有自动参照完整性",关系"这样的概念实际上是手动"的事情,实际上 .populate() 的情况实际上是一堆额外的查询以检索引用的信息.这里没有魔法".

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 .populate() is actually a whole bunch of additional queries in order to retrieve the referenced information. No "magic" here.

正确处理多对多"归结为三个选项:

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:

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
}

并产生整体输出,例如:

And produces overall output such as:

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
      }
    ]
  }
]

关键点在于您实际上将参考数据添加到存在关系的每个集合中的每个文档中.此处存在的数组"用于存储这些引用并查找"相关集合中的结果,并将它们替换为存储在那里的对象数据.

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.

注意以下部分:

// 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" 数组.这样做是为了可以从任一方向查询关系.如果您只想要来自商店的商品"并且从不来自商品的商店",那么您根本不需要将关系数据存储在商品"条目上.

Because that means not only are we adding the 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.

这本质上是经典的多对多"关系.这里不是直接定义两个集合之间的关系,而是另一个集合(表)存储有关哪个项目与哪个商店相关的详细信息.

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.

作为完整列表:

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();
  }

})();

关系现在在它们自己的集合中,所以数据现在显示不同,对于项目":

The relations are now in their own collection, so the data now appears differently, for "items":

{
    "_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"
}

现在是映射关系的storeitems":

And now for "storeitems" which maps the relations:

{
    "_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"
  }
]

由于关系现在映射到单独的集合中,因此这里有一些更改.值得注意的是,我们希望在集合上定义一个虚拟"字段,该字段不再具有固定的项目数组.所以你添加一个,如图所示:

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:

const itemSchema = new Schema({
  name: String,
},{
 toJSON: { virtuals: true }
});

itemSchema.virtual('stores', {
  ref: 'StoreItem',
  localField: '_id',
  foreignField: 'itemId'
});

您使用它的 localFieldforeignField 映射分配虚拟字段,以便随后的 .populate() 调用知道要使用什么.

You assign the virtual field with it's localField and foreignField mappings so the subsequent .populate() call knows what to use.

中间集合有一个相当标准的定义:

The intermediary collection has a fairly standard definition:

const storeItemSchema = new Schema({
  storeId: { type: Schema.Types.ObjectId, ref: 'Store', required: true },
  itemId: { type: Schema.Types.ObjectId, ref: 'Item', required: true }
});

我们不是将新项目推送"到数组中,而是将它们添加到这个新集合中.一种合理的方法是仅在此组合不存在时才使用upserts"创建新条目:

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:

// Add toothpaste to both stores
for( let store of [billsStore,tedsStore] ) {
  await StoreItem.update(
    { storeId: store._id, itemId: toothpaste._id },
    { },
    { 'upsert': true }
  );
}

这是一种非常简单的方法,它仅使用查询中提供的两个键创建一个新文档,其中一个未找到,或者本质上尝试在匹配时更新同一个文档,在这种情况下使用nothing".因此,现有的匹配最终会成为无操作",这是需要做的事情.或者,您可以简单地 .insertOne() 忽略重复键错误.随心所欲.

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 .insertOne() an ignore duplicate key errors. Whatever takes your fancy.

实际上查询这些相关"数据的工作方式又略有不同.因为涉及到另一个集合,我们调用 .populate() 的方式认为它也需要查找"其他检索到的属性上的关系.所以你有这样的电话:

Actually querying this "related" data works a little differently again. Because there is another collection involved, we call .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);

清单 3 - 在服务器上使用 Modern Features 来实现

因此,根据所采用的方法,使用数组或中间集合来存储关系数据作为文档中不断增长的数组"的替代方法,那么您应该注意的显而易见的事情是 .使用的 populate() 调用实际上是对 MongoDB 进行额外的查询,并在单独的请求中通过网络拉取这些文档.

Listing 3 - Use Modern Features to do it on the server

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 .populate() calls used are actually making additional queries to MongoDB and pulling those documents over the network in separate requests.

这在小剂量下可能看起来一切都很好,但是随着事情的扩大,尤其是请求数量的增加,这绝不是一件好事.此外,您可能还想应用其他条件,这意味着您不需要从服务器中提取所有文档,而是在返回结果之前匹配这些关系"中的数据.

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.

这就是为什么现代 MongoDB 版本包含 $lookup 实际上加入"服务器本身的数据.到目前为止,您应该已经查看了这些 API 调用产生的所有输出,如 mongoose.set('debug',true) 所示.

This is why modern MongoDB releases include $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).

所以这次我们不是产生多个查询,而是将其作为一条聚合语句在服务器上加入",并在一个请求中返回结果:

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:

// 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);

虽然编码时间更长,但即使对于这里非常微不足道的操作,实际上效率也高得多.这当然是相当可观的.

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:

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
      }
    ]
  }
]

显而易见的是,为了返回已连接"数据形式而发出的查询显着减少.这意味着由于消除了所有网络开销,延迟更低,应用程序响应速度更快.

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.

在所有情况下,如果您希望事情在双向"上起作用,则由您实际存储这些引用.当然 $lookup 甚至"virtuals",这意味着您并不总是需要存储在每个源上,因为您可以只在一个地方引用"并通过应用这些方法来使用该信息.

In all cases it is up to you to actually store those references if you expect things to work on "both directions". Of course $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.

另一种情况当然是嵌入",这是一个完全不同的游戏,以及 MongoDB 等面向文档的数据库的真正意义所在.因此,这个概念当然不是从另一个集合中获取",而是嵌入"数据.

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.

这不仅意味着指向其他项目的 ObjectId 值,而且实际上将完整数据存储在每个文档的数组中.当然存在大小"问题,当然还有在多个地方更新数据的问题.这通常是一个单个请求和一个简单请求的权衡,不需要去其他集合中查找数据,因为它已经存在".

This means not just the 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".

关于引用与嵌入的主题有很多材料.一旦这样的摘要来源是 Mongoose 填充 vs 对象嵌套,甚至是非常通用的 MongoDB 关系:嵌入还是引用? 等等.

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.

您应该花一些时间来思考这些概念以及如何将其应用于您的一般应用程序.请注意,您实际上并没有在这里使用 RDBMS,因此您最好使用您打算利用的正确功能,而不是简单地让一个行为像另一个一样.

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.

相关文章