sql >> データベース >  >> NoSQL >> MongoDB

マングースと多対多

    現在の問題は、一方のモデルに参照を保存したが、もう一方のモデルには保存しなかったことです。 MongoDBには「自動参照整合性」はなく、このような「関係」の概念は実際には「手動」の問題であり、実際には .populate()の場合です。 は実際には、参照された情報を取得するための追加のクエリの集まりです。ここには「魔法」はありません。

    「多対多」の正しい処理は、次の3つのオプションになります。

    リスト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
    });
    

    これは、歯磨き粉を追加するだけではないことを意味するためです。 "items"へ 各ストアに配列がありますが、各 "store"も追加しています "stores"歯磨き粉の配列 アイテム。これは、関係がどちらの方向からも照会されて機能できるようにするために行われます。 「店舗からのアイテム」のみが必要で、決して必要ない場合 「アイテムから保存」の場合、「アイテム」エントリにリレーションデータを保存する必要はまったくありません。

    リスト2-仮想および中間コレクションを使用する

    これは本質的に古典的な「多対多」の関係です。 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"
    }
    

    そして今、関係をマップする「storeitems」について:

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

    リレーションは別のコレクションにマップされるようになったため、ここでいくつかの変更があります。特に、アイテムの固定配列を持たなくなったコレクションの「仮想」フィールドを定義する必要があります。したがって、次のように1つ追加します。

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

    そして、新しいアイテムを配列に「プッシュ」する代わりに、この新しいコレクションに追加します。このための合理的な方法は、「アップサート」を使用して、この組み合わせが存在しない場合にのみ新しいエントリを作成することです。

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

    これは非常に単純な方法で、クエリで指定された2つのキーが見つからなかった場合に新しいドキュメントを作成するか、一致したときに同じドキュメントを更新しようとします。この場合は「なし」です。したがって、既存の一致は「ノーオペレーション」として終了します。これは、実行することが望ましいことです。または、 .insertOne()を使用することもできます。 重複キーエラーを無視します。好きなものは何でも。

    実際、この「関連する」データのクエリは、少し違った働きをします。別のコレクションが関係しているため、 .populate()を呼び出します。 他の取得されたプロパティの関係も「検索」する必要があると考える方法で。したがって、次のような呼び出しがあります:

     // Show stores
      let stores = await Store.find().populate({
        path: 'items',
        populate: { path: 'itemId' }
      });
      log(stores);
    

    リスト3-最新機能を使用してサーバー上で実行する

    したがって、どのアプローチを採用したかに応じて、ドキュメント内の「成長する配列」の代わりに、配列または中間コレクションを使用して関係データを格納する場合、注意すべき明らかなことは、 .populate() 使用される呼び出しは、実際にはMongoDBに追加のクエリを作成し、それらのドキュメントを別々のリクエストでネットワーク経由でプルしています。

    これは少量ですべてうまくいくように見えるかもしれませんが、物事がスケールアップし、特にリクエストの量が増えるにつれて、これは決して良いことではありません。さらに、適用したい他の条件があるかもしれません。つまり、サーバーからすべてのドキュメントをプルする必要はなく、結果を返す前にそれらの「関係」からのデータを照合する必要があります。

    これが、最新のMongoDBリリースに $ lookupが含まれている理由です。 これは実際にはサーバー自体のデータに「参加」します。これまでに、<​​code> mongoose.set('debug'、true)で示されるように、これらのAPI呼び出しが生成するすべての出力を確認しているはずです。 。

    したがって、複数のクエリを生成する代わりに、今回はサーバーに「参加」する1つの集計ステートメントを作成し、1つのリクエストで結果を返します。

    // 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 また、それが適用される「仮想」でさえ、1つの場所で「参照」し、それらのメソッドを適用することでその情報を使用できるため、必ずしもすべてのソースに保存する必要がないことを意味します。

    もう1つのケースはもちろん「埋め込み」です。これはまったく異なるゲームであり、MongoDBなどのドキュメント指向データベースが実際にどのようなものであるかを示しています。したがって、「別のコレクションからフェッチする」のではなく、もちろんデータを「埋め込む」という概念があります。

    これは、 ObjectIdだけではないことを意味します 他の項目を指す値ですが、実際には各ドキュメントの配列内に完全なデータを格納します。もちろん、「サイズ」の問題があり、もちろん、複数の場所でデータを更新することにも問題があります。これは通常、単一のリクエストがあることとのトレードオフです。 および簡単なリクエスト 「すでにそこにある」ので、他のコレクションのデータを探しに行く必要はありません。

    参照と埋め込みのテーマについては、たくさんの資料があります。そのような要約ソースがマングースの個体群とオブジェクトのネスト になると、 または、非常に一般的な MongoDBの関係:埋め込みまたは参照?> と他の多くの。

    概念と、これがアプリケーション全般に​​どのように適用されるかについて、時間をかけて考える必要があります。また、ここでは実際にはRDBMSを使用していないため、単に一方を他方のように動作させるのではなく、活用することを目的とした正しい機能を使用する方がよいことに注意してください。



    1. Mongo DBデザイン、埋め込みと関係

    2. ネストされた配列内のMongoDBクエリ

    3. MongoDBの上限付きコレクションで削除されたドキュメントを追跡する

    4. マングース:CastError:パス_idの値[オブジェクトオブジェクト]のObjectIdへのキャストに失敗しました