MongoDB
 sql >> Base de Dados >  >> NoSQL >> MongoDB

Muitos para muitos com Mangusto


O problema que você tem atualmente é que você salvou a referência em um modelo, mas não a salvou no outro. Não há "integridade referencial automática" no MongoDB, e tal conceito de "relações" é realmente um caso "manual", e de fato é o caso de .populate() é na verdade um monte de consultas adicionais para recuperar as informações referenciadas. Nada de "mágica" aqui.

O manuseio correto de "muitos para muitos" se resume a três opções:

Lista 1 - Manter matrizes em ambos os documentos


Seguindo seu projeto atual, as peças que faltam estão armazenando o referenciado em "ambos" os itens relacionados. Para uma listagem para demonstrar:
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();
  }

})();

Isso cria a coleção "itens":
{
    "_id" : ObjectId("59ab96d9c079220dd8eec428"),
    "name" : "toothpaste",
    "stores" : [
            ObjectId("59ab96d9c079220dd8eec42a"),
            ObjectId("59ab96d9c079220dd8eec42b")
    ],
    "__v" : 0
}
{
    "_id" : ObjectId("59ab96d9c079220dd8eec429"),
    "name" : "brush",
    "stores" : [
            ObjectId("59ab96d9c079220dd8eec42a")
    ],
    "__v" : 0
}

E a coleção "lojas":
{
    "_id" : ObjectId("59ab96d9c079220dd8eec42a"),
    "name" : "Bills",
    "items" : [
            ObjectId("59ab96d9c079220dd8eec428"),
            ObjectId("59ab96d9c079220dd8eec429")
    ],
    "__v" : 0
}
{
    "_id" : ObjectId("59ab96d9c079220dd8eec42b"),
    "name" : "Teds",
    "items" : [
            ObjectId("59ab96d9c079220dd8eec428")
    ],
    "__v" : 0
}

E produz saída geral, como:
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
      }
    ]
  }
]

Os pontos principais são que você realmente adiciona os dados de referência a cada documento em cada coleção onde existe um relacionamento. Os "arrays" presentes são usados ​​aqui para armazenar essas referências e "pesquisar" os resultados da coleção relacionada e substituí-los pelos dados do objeto que foram armazenados lá.

Preste atenção em peças como:
// Add items to stores
[billsStore,tedsStore].forEach( store => {
  store.items.push(toothpaste);   // add toothpaste to store
  toothpaste.stores.push(store);  // add store to toothpaste
});

Porque isso significa que não estamos apenas adicionando a toothpaste para os "items" array em cada loja, mas também estamos adicionando cada "store" para as "stores" matriz da toothpaste item. Isso é feito para que os relacionamentos possam funcionar sendo consultados em qualquer direção. Se você só queria "itens de lojas" e nunca "armazena de itens", então você não precisaria armazenar os dados de relação nas entradas de "item".

Listagem 2 - Use Virtuals e uma Coleção Intermediária


Esta é essencialmente a clássica relação "muitos para muitos". Onde ao invés de definir diretamente as relações entre as duas coleções, existe uma outra coleção ( table ) que armazena os detalhes sobre qual item está relacionado a qual loja.

Como listagem completa:
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();
  }

})();

As relações estão agora em sua própria coleção, então os dados agora aparecem de forma diferente, para "itens":
{
    "_id" : ObjectId("59ab996166d5cc0e0d164d74"),
    "__v" : 0,
    "name" : "toothpaste"
}
{
    "_id" : ObjectId("59ab996166d5cc0e0d164d75"),
    "__v" : 0,
    "name" : "brush"
}

E "lojas":
{
    "_id" : ObjectId("59ab996166d5cc0e0d164d76"),
    "__v" : 0,
    "name" : "Bills"
}
{
    "_id" : ObjectId("59ab996166d5cc0e0d164d77"),
    "__v" : 0,
    "name" : "Teds"
}

E agora para "storeitems" que mapeia as relações:
{
    "_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
}

Com saída completa como:
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"
  }
]

Como as relações agora estão mapeadas em uma coleção separada, há algumas mudanças aqui. Notavelmente, queremos definir um campo "virtual" na coleção que não tenha mais uma matriz fixa de itens. Então você adiciona um como é mostrado:
const itemSchema = new Schema({
  name: String,
},{
 toJSON: { virtuals: true }
});

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

Você atribui o campo virtual com seu localField e foreignField mapeamentos para que o .populate() subsequente call sabe o que usar.

A coleção intermediária tem uma definição bastante padrão:
const storeItemSchema = new Schema({
  storeId: { type: Schema.Types.ObjectId, ref: 'Store', required: true },
  itemId: { type: Schema.Types.ObjectId, ref: 'Item', required: true }
});

E em vez de "empurrar" novos itens para arrays, nós os adicionamos a esta nova coleção. Um método razoável para isso é usar "upserts" para criar uma nova entrada somente quando essa combinação não existir:
// Add toothpaste to both stores
for( let store of [billsStore,tedsStore] ) {
  await StoreItem.update(
    { storeId: store._id, itemId: toothpaste._id },
    { },
    { 'upsert': true }
  );
}

É um método bastante simples que apenas cria um novo documento com as duas chaves fornecidas na consulta onde uma não foi encontrada, ou essencialmente tenta atualizar o mesmo documento quando combinado e com "nada" neste caso. Portanto, as partidas existentes acabam como "no-op", que é a coisa desejada a fazer. Alternativamente, você pode simplesmente .insertOne() um ignorar erros de chave duplicada. O que quer que você goste.

Na verdade, consultar esses dados "relacionados" funciona um pouco diferente novamente. Como há outra coleção envolvida, chamamos .populate() de uma forma que considere que precisa "pesquisar" a relação em outra propriedade recuperada também. Então você tem chamadas assim:
 // Show stores
  let stores = await Store.find().populate({
    path: 'items',
    populate: { path: 'itemId' }
  });
  log(stores);

Listagem 3 - Use recursos modernos para fazer isso no servidor


Então, dependendo de qual abordagem adotada, usando arrays ou uma coleção intermediária para armazenar os dados da relação como uma alternativa para "arrays crescentes" dentro dos documentos, então a coisa óbvia que você deve observar é que o .populate() chamadas usadas estão realmente fazendo consultas adicionais ao MongoDB e puxando esses documentos pela rede em solicitações separadas.

Isso pode parecer muito bom em pequenas doses, no entanto, à medida que as coisas aumentam e especialmente em volumes de solicitações, isso nunca é uma coisa boa. Além disso, pode haver outras condições que você deseja aplicar, o que significa que você não precisa extrair todos os documentos do servidor e prefere corresponder os dados dessas "relações" antes de retornar os resultados.

É por isso que as versões modernas do MongoDB incluem $lookup que realmente "junta" os dados no próprio servidor. Até agora você deve estar olhando para toda a saída que essas chamadas de API produzem, como mostrado por mongoose.set('debug',true) .

Então, em vez de produzir várias consultas, desta vez fazemos uma declaração de agregação para "ingressar" no servidor e retornar os resultados em uma solicitação:
// 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);

O que, embora mais longo na codificação, é realmente muito superior em eficiência, mesmo para a ação muito trivial aqui. Isso, claro, escala consideravelmente.

Seguindo o mesmo modelo "intermediário" de antes (e apenas por exemplo, porque poderia ser feito de qualquer maneira) temos uma listagem completa:
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();
  }

})()

E a saída:
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
      }
    ]
  }
]

O que deve ser óbvio é a redução significativa nas consultas emitidas no final para retornar a forma "junta" dos dados. Isso significa menor latência e aplicativos mais responsivos como resultado da remoção de toda a sobrecarga da rede.

Observações finais


Essas são geralmente suas abordagens para lidar com relações "muitos para muitos", que basicamente se resumem a:

  • Mantendo arrays em cada documento em ambos os lados mantendo as referências aos itens relacionados.

  • Armazenar uma coleção intermediária e usá-la como referência de pesquisa para recuperar os outros dados.

Em todos os casos, você decide para realmente armazenar essas referências se você espera que as coisas funcionem em "ambas as direções". Claro $lookup e até mesmo "virtuais" onde isso se aplica significa que você nem sempre precisa armazenar em todas as fontes, pois você poderia "referenciar" em apenas um lugar e usar essas informações aplicando esses métodos.

O outro caso é, claro, "embedding", que é um jogo totalmente diferente e do que se tratam os bancos de dados orientados a documentos, como o MongoDB. Portanto, em vez de "buscar de outra coleção", o conceito é, obviamente, "incorporar" os dados.

Isso significa não apenas o ObjectId valores que apontam para os outros itens, mas na verdade armazenam os dados completos em arrays em cada documento. É claro que há um problema de "tamanho" e, claro, problemas com a atualização de dados em vários lugares. Geralmente, essa é a compensação por haver uma solicitação única e uma solicitação simples que não precisa ir buscar dados em outras coleções porque "já está lá".

Há muito material disponível sobre o assunto de referência versus incorporação. Quando essa fonte de resumo for Mongoose populate vs object nesting ou até mesmo os relacionamentos muito gerais MongoDB:incorporar ou fazer referência? e muitos muitos outros.

Você deve gastar algum tempo pensando sobre os conceitos e como isso se aplica ao seu aplicativo em geral. E note que você não está realmente usando um RDBMS aqui, então você pode usar os recursos corretos que você deve explorar, em vez de simplesmente fazer um agir como o outro.