Поиск, когда ForeignField находится в массиве - PullRequest
0 голосов
/ 22 мая 2018

Я хочу перейти от объекта к коллекции, в которой ключ foreignField встроен в массив объектов.У меня есть:

коллекция "рубашки"

{ 
      "_id" : ObjectId("5a797ef0768d8418866eb0f6"), 
      "name" : "Supermanshirt", 
      "price" : 9.99,
      "flavours" : [
                     {
                       "flavId" : ObjectId("5a797f8c768d8418866ebad3"), 
                       "size" : "M", 
                       "color": "white",
                     },
                     {
                       "flavId" : ObjectId("3a797f8c768d8418866eb0f7"), 
                       "size" : "XL", 
                       "color": "red",
                     }, 
                   ]
}

коллекция "корзина"

 { 
      "_id" : ObjectId("5a797ef0333d8418866ebabc"), 
      "basketName" : "Default", 
      "items" : [
                   {
                      "dateAdded" : 1526996879787.0, 
                      "itemFlavId" : ObjectId("5a797f8c768d8418866ebad3")
                   }
                ], 
}

Мой запрос:

basketSchema.aggregate([
                    {
                       $match: { $and: [{ _id }, { basketName }]},
                    },
                    {
                       $unwind: '$items',
                    },
                    {
                       $lookup:
                       {
                         from: 'shirts',
                         localField: 'items.itemFlavId',
                         foreignField: 'flavours.flavId',
                         as: 'ordered_shirts',
                       },
                    },
                    ]).toArray();

мой ожидаемый результат:

[{ 
  "_id" : ObjectId("5a797ef0333d8418866ebabc"), 
  "basketName" : "Default", 
  "items" : [
               {
                  "dateAdded" : 1526996879787.0, 
                  "itemFlavId" : ObjectId("5a797f8c768d8418866ebad3")
               }
            ], 
   "ordered_shirts" : [
                     { 
                        "_id" : ObjectId("5a797ef0768d8418866eb0f6"), 
                        "name" : "Supermanshirt", 
                        "price" : 9.99,
                        "flavours" : [
                                {
                                   "flavId" : ObjectId("5a797f8c768d8418866ebad3"), 
                                   "size" : "M", 
                                   "color": "white",
                                }
                   ]
}
            ], 
}]

но вместо этого мой массив order_shirts пуст.

Как я могу использовать foreignField, если этот foreignField встроен в массив другой коллекции?

Я использую MongoDB 3.6.4

1 Ответ

0 голосов
/ 23 мая 2018

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

Конечногде вам нужно выполнить такой запрос «после», что начальная стадия $lookup не простая задача.С структурной точки зрения то, что у вас есть, обычно не очень хорошая идея, так как ссылка «соединения» на элементы в массиве означает, что вы всегда возвращаете данные, которые не обязательно «связаны».

Есть несколько способов борьбыи, в основном, существует форма «некоррелированного» * ​​1008 *$lookup, введенная в MongoDB 3.6, которая может помочь вам гарантировать, что вы не возвращаете «ненужные» данные в «объединении».

Я работаю здесь в форме «слияния» детали «sku» с "items" в корзине, поэтому первая форма будет:

Optimal MongoDB 3.6

// Store some vars like you have
let _id = ObjectId("5a797ef0333d8418866ebabc"),
    basketName = "Default";

// Run non-correlated $lookup
let optimal = await Basket.aggregate([
  { "$match": { _id, basketName } },
  { "$lookup": {
    "from": Shirt.collection.name,
    "as": "items",
    "let": { "items": "$items" },
    "pipeline": [
      { "$match": {
        "$expr": {
          "$setIsSubset": ["$$items.itemflavId", "$flavours.flavId"]
        }
      }},
      { "$project": {
        "_id": 0,
        "items": {
          "$map": {
            "input": {
              "$filter": {
                "input": "$flavours",
                "cond": { "$in": [ "$$this.flavId", "$$items.itemFlavId" ]}
              }
            },
            "in": {
              "$mergeObjects": [
                { "$arrayElemAt": [
                  "$$items",
                  { "$indexOfArray": [
                    "$$items.itemFlavId", "$$this.flavId" ] }
                ]},
                { "name": "$name", "price": "$price" },
                "$$this"
              ]
            }
          }
        }
      }},
      { "$unwind": "$items" },
      { "$replaceRoot": {  "newRoot": "$items" } }
    ]
  }}
])

Обратите внимание, что, поскольку вы используете mongoose для хранения сведений о моделях, мы можем использовать Shirt.collection.name здесь, чтобы прочитать свойство этой модели с фактическим именем коллекции, необходимым для $lookup.Это помогает избежать путаницы в коде, а также «жестко кодировать» что-то вроде имени коллекции, когда она фактически хранится где-то еще.Таким образом, если вы измените код, который регистрирует «модель» таким образом, который изменил имя коллекции, то это всегда будет возвращать правильное имя для использования на стадии конвейера.

Основная причина, по которой вы используете этоФорма $lookup с MongoDB 3.6 объясняется тем, что вы хотите использовать этот «конвейер» для манипулирования результатами сторонней коллекции «до того», как они будут возвращены и объединены с родительским документом.Так как мы «объединяем» результаты в существующий массив корзины "items", мы используем то же имя поля в аргументе для "as".

В этой форме $lookupОбычно вы все еще хотите «связанные» документы, даже если это дает вам возможность делать все, что вы хотите.В этом случае мы можем сравнить содержимое массива с "items" в родительском документе, который мы установили в качестве переменной для конвейера, чтобы использовать с массивом под "flavours" в чужой коллекции.Логическое сравнение для двух «наборов» значений здесь, где они «пересекаются», использует оператор $setIsSubset, используя $expr, поэтому мы можем сравнивать «логическую операцию»".

Основная работа здесь выполняется в $project, который просто использует $map в массиве из массива "flavours" стороннего документа, обработанного с помощью $filter по сравнению с "items", который мы передали в конвейер и по существу переписали для «слияния» соответствующего содержимого.

$filter сокращает список для рассмотрения только до тех, которые соответствуют чему-то, присутствующему в "items", а затем мы можем использовать $indexOfArray и $arrayElemAt, чтобы извлечь деталииз "items" и объедините его с каждой оставшейся записью "flavours", которая соответствует с помощью оператора $mergeObjects.Отметив здесь, что мы также берем некоторые «родительские» детали из «рубашки» как поля "name" и "price", которые являются общими для вариаций размера и цвета.

Поскольку это все еще массив«в сопоставленном документе (ах) с условием соединения, чтобы получить« плоский список »объектов, подходящих для« объединенных »записей в результирующем "items" из $lookup, мы просто применяем$unwind, который в контексте сопоставленных оставленных элементов создает только «небольшие» накладные расходы, и $replaceRoot для продвижения содержимого этого ключа на верхний уровень.

Результатом является просто «объединенный» контент, указанный в "items" корзины.

Неоптимальный MongoDB

Альтернативные подходы на самом деле не так уж хороши, поскольку все они включают в себя возвращение других «ароматов», которые на самом деле не соответствуют товарам в корзине.Это в основном включает в себя «постфильтрацию» результатов, полученных из $lookup, в отличие от «предварительной фильтрации», что и процесс, описанный выше.

Так что следующий случай здесь будет использоватьметоды манипулирования возвращаемым массивом для удаления элементов, которые на самом деле не совпадают:

// Using legacy $lookup
let alternate = await Basket.aggregate([
  { "$match": { _id, basketName } },
  { "$lookup": {
    "from": Shirt.collection.name,
    "localField": "items.itemFlavId",
    "foreignField": "flavours.flavId",
    "as": "ordered_items"
  }},
  { "$addFields": {
    "items": {
      "$let": {
        "vars": {
          "ordered_items": {
            "$reduce": {
              "input": {
                "$map": {
                  "input": "$ordered_items",
                  "as": "o",
                  "in": {
                    "$map": {
                      "input": {
                        "$filter": {
                          "input": "$$o.flavours",
                          "cond": {
                            "$in": ["$$this.flavId", "$items.itemFlavId"]
                          }
                        }
                      },
                      "as": "f",
                      "in": {
                        "$mergeObjects": [
                          { "name": "$$o.name", "price": "$$o.price" },
                          "$$f"
                        ]
                      }
                    }
                  }
                }
              },
              "initialValue": [],
              "in": { "$concatArrays": ["$$value", "$$this"] }
            }
          }
        },
        "in": {
          "$map": {
            "input": "$items",
            "in": {
              "$mergeObjects": [
                "$$this",
                { "$arrayElemAt": [
                  "$$ordered_items",
                  { "$indexOfArray": [
                    "$$ordered_items.flavId", "$$this.itemFlavId"
                  ]}
                ]}
              ]
            }
          }
        }
      }
    },
    "ordered_items": "$$REMOVE"
  }}
]);

Здесь я все еще использую некоторые функции MongoDB 3.6, но это не «требование» логикиучаствует.Основным ограничением в этом подходе является $reduce, который требует MongoDB 3.4 или выше.

Используя ту же «устаревшую» форму $lookup, что и вы, мы все равно получаемтребуемые результаты при отображении, но, конечно, они содержат информацию в "flavours", которая не соответствует "items" в корзине.Во многом так же, как показано в предыдущем списке, мы можем применить $filter здесь, чтобы удалить элементы, которые не соответствуют.В этом же процессе используется вывод $filter в качестве ввода для $map, который снова выполняет тот же процесс «слияния», что и раньше.

Где$reduce вступает в силу потому, что результирующая обработка, где есть цель «массива» из $lookup с документами, которые сами содержат "array" из "flavours", заключается в том, чтоэти массивы необходимо «объединить» в один массив для дальнейшей обработки.$reduce просто использует обработанный вывод и выполняет $concatArrays для каждого из возвращаемых «внутренних» массивов, чтобы сделать эти результаты единичными.Мы уже «объединили» контент, так что это становится новым «объединенным» "items".

Старше Все еще $ unwind

И, конечно, последний способ представления (хотя есть и другиекомбинаций) использует $unwind для массивов и использует $group, чтобы собрать его вместе:

let old = await Basket.aggregate([
  { "$match": { _id, basketName } },
  { "$unwind": "$items" },
  { "$lookup": {
    "from": Shirt.collection.name,
    "localField": "items.itemFlavId",
    "foreignField": "flavours.flavId",
    "as": "ordered_items"
  }},
  { "$unwind": "$ordered_items" },
  { "$unwind": "$ordered_items.flavours" },
  { "$redact": {
    "$cond": {
      "if": {
        "$eq": [
          "$items.itemFlavId",
          "$ordered_items.flavours.flavId"
        ]
      },
      "then": "$$KEEP",
      "else": "$$PRUNE"
    }
  }},
  { "$group": {
    "_id": "$_id",
    "basketName": { "$first": "$basketName" },
    "items": {
      "$push": {
        "dateAdded": "$items.dateAdded",
        "itemFlavId": "$items.itemFlavId",
        "name": "$ordered_items.name",
        "price": "$ordered_items.price",
        "flavId": "$ordered_items.flavours.flavId",
        "size": "$ordered_items.flavours.size",
        "color": "$ordered_items.flavours.color"
      }
    }
  }}
]);

Большая часть этого должна быть довольно понятнаas $unwind - это просто инструмент для "выравнивания" содержимого массива в единичных записях документа.Чтобы просто получить желаемые результаты, мы можем использовать $redact для сравнения двух полей.Используя MongoDB 3.6, вы «могли» использовать $expr в пределах $match здесь:

{ "$match": {
  "$expr": {
    "$eq": [
      "$items.itemFlavId",
      "$ordered_items.flavours.flavId"
    ]
  }
}}

Но когда дело доходит до этого, если у вас естьMongoDB 3.6 с его другими функциями, тогда $unwind - это неправильная вещь, которую нужно здесь делать из-за всех накладных расходов, которые она фактически добавит.

Так что все, что действительно происходит, это вы $lookup затем «свести» документы и, наконец, $group все связанные детали вместе, используя $push для воссоздания "items" в корзине.Он «выглядит просто» и, вероятно, является наиболее легкой для понимания формой, однако «простота» не равна «производительности», и это было бы довольно брутально для использования в реальных условиях использования.

Резюме

Это должно охватывать объяснение того, что вам нужно делать при работе с «объединениями», которые будут сравнивать элементы в массивах.Это, вероятно, должно привести вас к пути осознания того, что это не очень хорошая идея, и было бы гораздо лучше держать свой «skus» в списке «по отдельности», а не перечислять их все как один «элемент».

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

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


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

const { Schema, Types: { ObjectId } } = mongoose = require('mongoose');

const uri = 'mongodb://localhost/basket';

mongoose.Promise = global.Promise;
mongoose.set('debug', true);


const basketItemSchema = new Schema({
  dateAdded: { type: Number, default: Date.now() },
  itemFlavId: { type: Schema.Types.ObjectId }
},{ _id: false });

const basketSchema = new Schema({
  basketName: String,
  items: [basketItemSchema]
});

const flavourSchema = new Schema({
  flavId: { type: Schema.Types.ObjectId },
  size: String,
  color: String
},{ _id: false });

const shirtSchema = new Schema({
  name: String,
  price: Number,
  flavours: [flavourSchema]
});

const Basket = mongoose.model('Basket', basketSchema);
const Shirt = mongoose.model('Shirt', shirtSchema);

const log = data => console.log(JSON.stringify(data, undefined, 2));

(async function() {

  try {

    const conn = await mongoose.connect(uri);

    // clean data
    await Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()));

    // set up data for test
    await Basket.create({
      _id: ObjectId("5a797ef0333d8418866ebabc"),
      basketName: "Default",
      items: [
        {
          dateAdded: 1526996879787.0,
          itemFlavId: ObjectId("5a797f8c768d8418866ebad3")
        }
      ]
    });

    await Shirt.create({
      _id: ObjectId("5a797ef0768d8418866eb0f6"),
      name: "Supermanshirt",
      price: 9.99,
      flavours: [
        {
          flavId: ObjectId("5a797f8c768d8418866ebad3"),
          size: "M",
          color: "white"
        },
        {
          flavId: ObjectId("3a797f8c768d8418866eb0f7"),
          size: "XL",
          color: "red"
        }
      ]
    });

    // Store some vars like you have
    let _id = ObjectId("5a797ef0333d8418866ebabc"),
        basketName = "Default";

    // Run non-correlated $lookup
    let optimal = await Basket.aggregate([
      { "$match": { _id, basketName } },
      { "$lookup": {
        "from": Shirt.collection.name,
        "as": "items",
        "let": { "items": "$items" },
        "pipeline": [
          { "$match": {
            "$expr": {
              "$setIsSubset": ["$$items.itemflavId", "$flavours.flavId"]
            }
          }},
          { "$project": {
            "_id": 0,
            "items": {
              "$map": {
                "input": {
                  "$filter": {
                    "input": "$flavours",
                    "cond": { "$in": [ "$$this.flavId", "$$items.itemFlavId" ]}
                  }
                },
                "in": {
                  "$mergeObjects": [
                    { "$arrayElemAt": [
                      "$$items",
                      { "$indexOfArray": [
                        "$$items.itemFlavId", "$$this.flavId" ] }
                    ]},
                    { "name": "$name", "price": "$price" },
                    "$$this"
                  ]
                }
              }
            }
          }},
          { "$unwind": "$items" },
          { "$replaceRoot": {  "newRoot": "$items" } }
        ]
      }}
    ])

    log(optimal);

    // Using legacy $lookup
    let alternate = await Basket.aggregate([
      { "$match": { _id, basketName } },
      { "$lookup": {
        "from": Shirt.collection.name,
        "localField": "items.itemFlavId",
        "foreignField": "flavours.flavId",
        "as": "ordered_items"
      }},
      { "$addFields": {
        "items": {
          "$let": {
            "vars": {
              "ordered_items": {
                "$reduce": {
                  "input": {
                    "$map": {
                      "input": "$ordered_items",
                      "as": "o",
                      "in": {
                        "$map": {
                          "input": {
                            "$filter": {
                              "input": "$$o.flavours",
                              "cond": {
                                "$in": ["$$this.flavId", "$items.itemFlavId"]
                              }
                            }
                          },
                          "as": "f",
                          "in": {
                            "$mergeObjects": [
                              { "name": "$$o.name", "price": "$$o.price" },
                              "$$f"
                            ]
                          }
                        }
                      }
                    }
                  },
                  "initialValue": [],
                  "in": { "$concatArrays": ["$$value", "$$this"] }
                }
              }
            },
            "in": {
              "$map": {
                "input": "$items",
                "in": {
                  "$mergeObjects": [
                    "$$this",
                    { "$arrayElemAt": [
                      "$$ordered_items",
                      { "$indexOfArray": [
                        "$$ordered_items.flavId", "$$this.itemFlavId"
                      ]}
                    ]}
                  ]
                }
              }
            }
          }
        },
        "ordered_items": "$$REMOVE"
      }}
    ]);
    log(alternate);

    // Or really old style

    let old = await Basket.aggregate([
      { "$match": { _id, basketName } },
      { "$unwind": "$items" },
      { "$lookup": {
        "from": Shirt.collection.name,
        "localField": "items.itemFlavId",
        "foreignField": "flavours.flavId",
        "as": "ordered_items"
      }},
      { "$unwind": "$ordered_items" },
      { "$unwind": "$ordered_items.flavours" },
      { "$redact": {
        "$cond": {
          "if": {
            "$eq": [
              "$items.itemFlavId",
              "$ordered_items.flavours.flavId"
            ]
          },
          "then": "$$KEEP",
          "else": "$$PRUNE"
        }
      }},
      { "$group": {
        "_id": "$_id",
        "basketName": { "$first": "$basketName" },
        "items": {
          "$push": {
            "dateAdded": "$items.dateAdded",
            "itemFlavId": "$items.itemFlavId",
            "name": "$ordered_items.name",
            "price": "$ordered_items.price",
            "flavId": "$ordered_items.flavours.flavId",
            "size": "$ordered_items.flavours.size",
            "color": "$ordered_items.flavours.color"
          }
        }
      }}
    ]);

    log(old);


  } catch(e) {
    console.error(e)
  } finally {
    process.exit()
  }

})()

И пример вывода как:

Mongoose: baskets.remove({}, {})
Mongoose: shirts.remove({}, {})
Mongoose: baskets.insertOne({ _id: ObjectId("5a797ef0333d8418866ebabc"), basketName: 'Default', items: [ { dateAdded: 1526996879787, itemFlavId: ObjectId("5a797f8c768d8418866ebad3") } ], __v: 0 })
Mongoose: shirts.insertOne({ _id: ObjectId("5a797ef0768d8418866eb0f6"), name: 'Supermanshirt', price: 9.99, flavours: [ { flavId: ObjectId("5a797f8c768d8418866ebad3"), size: 'M', color: 'white' }, { flavId: ObjectId("3a797f8c768d8418866eb0f7"), size: 'XL', color: 'red' } ], __v: 0 })
Mongoose: baskets.aggregate([ { '$match': { _id: 5a797ef0333d8418866ebabc, basketName: 'Default' } }, { '$lookup': { from: 'shirts', as: 'items', let: { items: '$items' }, pipeline: [ { '$match': { '$expr': { '$setIsSubset': [ '$$items.itemflavId', '$flavours.flavId' ] } } }, { '$project': { _id: 0, items: { '$map': { input: { '$filter': { input: '$flavours', cond: { '$in': [Array] } } }, in: { '$mergeObjects': [ { '$arrayElemAt': [Array] }, { name: '$name', price: '$price' }, '$$this' ] } } } } }, { '$unwind': '$items' }, { '$replaceRoot': { newRoot: '$items' } } ] } } ], {})
[
  {
    "_id": "5a797ef0333d8418866ebabc",
    "basketName": "Default",
    "items": [
      {
        "dateAdded": 1526996879787,
        "itemFlavId": "5a797f8c768d8418866ebad3",
        "name": "Supermanshirt",
        "price": 9.99,
        "flavId": "5a797f8c768d8418866ebad3",
        "size": "M",
        "color": "white"
      }
    ],
    "__v": 0
  }
]
Mongoose: baskets.aggregate([ { '$match': { _id: 5a797ef0333d8418866ebabc, basketName: 'Default' } }, { '$lookup': { from: 'shirts', localField: 'items.itemFlavId', foreignField: 'flavours.flavId', as: 'ordered_items' } }, { '$addFields': { items: { '$let': { vars: { ordered_items: { '$reduce': { input: { '$map': { input: '$ordered_items', as: 'o', in: { '$map': [Object] } } }, initialValue: [], in: { '$concatArrays': [ '$$value', '$$this' ] } } } }, in: { '$map': { input: '$items', in: { '$mergeObjects': [ '$$this', { '$arrayElemAt': [ '$$ordered_items', [Object] ] } ] } } } } }, ordered_items: '$$REMOVE' } } ], {})
[
  {
    "_id": "5a797ef0333d8418866ebabc",
    "basketName": "Default",
    "items": [
      {
        "dateAdded": 1526996879787,
        "itemFlavId": "5a797f8c768d8418866ebad3",
        "name": "Supermanshirt",
        "price": 9.99,
        "flavId": "5a797f8c768d8418866ebad3",
        "size": "M",
        "color": "white"
      }
    ],
    "__v": 0
  }
]
Mongoose: baskets.aggregate([ { '$match': { _id: 5a797ef0333d8418866ebabc, basketName: 'Default' } }, { '$unwind': '$items' }, { '$lookup': { from: 'shirts', localField: 'items.itemFlavId', foreignField: 'flavours.flavId', as: 'ordered_items' } }, { '$unwind': '$ordered_items' }, { '$unwind': '$ordered_items.flavours' }, { '$redact': { '$cond': { if: { '$eq': [ '$items.itemFlavId', '$ordered_items.flavours.flavId' ] }, then: '$$KEEP', else: '$$PRUNE' } } }, { '$group': { _id: '$_id', basketName: { '$first': '$basketName' }, items: { '$push': { dateAdded: '$items.dateAdded', itemFlavId: '$items.itemFlavId', name: '$ordered_items.name', price: '$ordered_items.price', flavId: '$ordered_items.flavours.flavId', size: '$ordered_items.flavours.size', color: '$ordered_items.flavours.color' } } } } ], {})
[
  {
    "_id": "5a797ef0333d8418866ebabc",
    "basketName": "Default",
    "items": [
      {
        "dateAdded": 1526996879787,
        "itemFlavId": "5a797f8c768d8418866ebad3",
        "name": "Supermanshirt",
        "price": 9.99,
        "flavId": "5a797f8c768d8418866ebad3",
        "size": "M",
        "color": "white"
      }
    ]
  }
]
...