Много ко многим с мангустом

1

У меня две модели:

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 сохраняется с массивом items содержащим 1 item. item, тем не менее, не имеет stores. Что мне не хватает? Как автоматически обновлять отношения "многие ко многим" в MongoDB/Mongoose? Я привык к Rails, и все было сделано автоматически.

  • 1
    Mongodb - это база данных NoSql, отношения « many-to-many . Каждый документ независим.
  • 0
    Суть в том, что вы, кажется, ожидаете, что что-то будет «автоматически ссылаться» с «другой стороны», потому что вы добавили ссылку «с одной стороны». Документы являются независимыми, и если вы ожидаете, что «магазин» будет присутствовать и в «элементе», то вам необходимо вручную добавить его аналогичным образом. Здесь нет внешних наборов правил для ссылочной целостности. Только те данные, которые фактически находятся в самих документах.
Показать ещё 1 комментарий
Теги:
mongoose
nosql

2 ответа

9

В настоящее время проблема заключается в том, что вы сохранили ссылку в одной модели, но не сохранили ее в другой. В 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();
  }

})();

Это создает коллекцию "items":

{
    "_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" в массив "stores" элемента toothpaste. Это сделано для того, чтобы отношения могли запрашиваться в любом направлении. Если вам нужны только "товары из магазинов", а не "товары из товаров", то вам вообще не нужно хранить данные отношений в записях "товаров".

Листинг 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"
  }
]

Поскольку отношения теперь отображаются в отдельной коллекции, здесь есть пара изменений. В частности, мы хотим определить "виртуальное" поле в коллекции, которое больше не имеет фиксированного массива элементов. Таким образом, вы добавляете один, как показано:

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

Это довольно простой метод, который просто создает новый документ с двумя ключами, предоставленными в запросе, где один не был найден, или, по сути, пытается обновить тот же документ при сопоставлении, и с "ничем" в этом случае. Таким образом, существующие совпадения просто оказываются "без операции", что является желаемой вещью. С другой стороны, вы можете просто .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 и даже "virtuals", где это применимо, означает, что вам не всегда нужно хранить данные в каждом источнике, поскольку вы можете "ссылаться" только в одном месте и использовать эту информацию, применяя эти методы.

Другой случай - это, конечно, "встраивание", это совершенно другая игра, и для этого действительно нужны базы данных, ориентированные на документы, такие как MongoDB. Следовательно, вместо "выборки из другой коллекции" концепция заключается в том, чтобы "встраивать" данные.

Это означает не только значения ObjectId которые указывают на другие элементы, но и фактическое хранение полных данных в массивах в каждом документе. Конечно, существует проблема "размера" и, конечно, проблемы с обновлением данных в нескольких местах. Как правило, это компромисс за то, что существует один запрос и простой запрос, который не должен идти и находить данные в других коллекциях, потому что они "уже есть".

Существует множество материалов на тему ссылок на встраивание. Когда-то таким суммарным источником будет заполнение Mongoose против вложенности объектов или даже очень общие отношения MongoDB: встраивать или ссылаться? и многое, многое другое.

Вам следует потратить некоторое время на размышления о концепциях и о том, как это относится к вашему приложению в целом. И обратите внимание, что вы на самом деле здесь не используете СУБД, так что вы могли бы также использовать правильные функции, которые вы должны использовать, вместо того, чтобы просто заставить одно действовать как другое.

0

Сначала вы должны рассмотреть использование данных в своем приложении до моделирования базы данных.

У меня нет подробных требований к вашему приложению. Но почему вы должны сохранить 2 ссылки в 2 схемах? Почему бы не просто сохранить 1 ссылку от Store to Item (что означает, что 1 магазин имеет много элементов), а затем, если вы хотите выполнить запрос, чтобы определить, к каким магазинам принадлежит элемент, вам также нужно сделать это, запросив коллекцию Store.

Кроме того, в MongoDB ничего не называют "многие-ко-многим". Это зависит от того, как используются данные, чтобы вы могли найти эффективный способ формирования отношений между коллекциями, а также для структурирования вашей базы данных.

Во всяком случае, если вы все еще хотите использовать свои текущие схемы, вы можете сначала создать элемент, затем создать хранилище и нажать идентификатор созданного элемента в массив items, а затем выполнить обновление элемента с созданным идентификатором магазина.

  • 0
    Я согласен с вами, что, возможно, достаточно иметь ссылку в Store на Item . Как мне запросить все магазины, которые имеют конкретное имя item.name на основе поискового запроса?
  • 0
    Затем у вас есть 2 варианта: (1) встроить item.name в массив items и принять избыточность для операций быстрого чтения; (2) найдите item._id из коллекции Items , затем найдите этот _id в массиве items коллекции Stores . Так называемое заполнение - это на самом деле другой запрос, и это просто синтаксический сахар.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню