Как прокомментировано, может показаться, что в вашем коде есть что-то, что указывает на неправильную коллекцию.В общем случае для этого просто посмотрите на приведенный ниже пример списка и посмотрите, в чем заключаются различия, поскольку с предоставленными вами данными и правильными именами коллекций ваш ожидаемый результат фактически возвращается.
Конечногде вам нужно выполнить такой запрос «после», что начальная стадия $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"
}
]
}
]