Это действительно не очень хорошая структура для работы, и для этого есть очень веские причины. Таким образом, выполнение $lookup
здесь не простая задача, поскольку существуют различные последствия использования «вложенных массивов»
Вы в основном хотите либо
db.users.aggregate([
{ "$match": { "name": "Michael" } },
{ "$lookup": {
"from": "schools",
"localField": "starred",
"foreignField": "faculties.subjects.id",
"as": "subjects"
}},
{ "$addFields": {
"subjects": {
"$filter": {
"input": {
"$reduce": {
"input": {
"$reduce": {
"input": "$subjects.faculties.subjects",
"initialValue": [],
"in": { "$concatArrays": [ "$$value", "$$this" ] }
}
},
"initialValue": [],
"in": { "$concatArrays": [ "$$value", "$$this" ] }
}
},
"cond": { "$in": ["$$this.id", "$starred"] }
}
}
}}
])
Или с MongoDB 3.6 или выше, может быть вместо:
db.users.aggregate([
{ "$match": { "name": "Michael" } },
{ "$lookup": {
"from": "schools",
"let": { "starred": "$starred" },
"pipeline": [
{ "$match": {
"$expr": {
"$setIsSubset": [
"$$starred",
{ "$reduce": {
"input": "$faculties.subjects.id",
"initialValue": [],
"in": { "$concatArrays": [ "$$value", "$$this" ] }
}}
]
}
}},
{ "$project": {
"_id": 0,
"subjects": {
"$filter": {
"input": {
"$reduce": {
"input": "$faculties.subjects",
"initialValue": [],
"in": { "$concatArrays": [ "$$value", "$$this" ] }
}
},
"cond": { "$in": [ "$$this.id", "$$starred" ] }
}
}
}},
{ "$unwind": "$subjects" },
{ "$replaceRoot": { "newRoot": "$subjects" } }
],
"as": "subjects"
}}
])
Оба подхода в основном полагаются на $reduce
и $concatArrays
для того, чтобы «сгладить» содержимое «вложенного массива» в форму, которую можно использовать для сравнения. Основное различие между ними заключается в том, что до MongoDB 3.6 вы, по сути, извлекали все «возможные» совпадения из документа, прежде чем можно было что-либо делать с «фильтрацией» записей внутреннего массива только по тем, которые совпадают.
Если у вас не меньше MongoDB 3.4 с операторами $reduce
и $in
, то вы, по сути, прибегаете к $unwind
:
db.users.aggregate([
{ "$match": { "name": "Michael" } },
{ "$lookup": {
"from": "schools",
"localField": "starred",
"foreignField": "faculties.subjects.id",
"as": "subjects"
}},
{ "$unwind": "$subjects" },
{ "$unwind": "$subjects.faculties" },
{ "$unwind": "$subjects.faculties.subjects" },
{ "$redact": {
"$cond": {
"if": {
"$setIsSubset": [
["$subjects.faculties.subjects.id"],
"$starred"
]
},
"then": "$$KEEP",
"else": "$$PRUNE"
}
}},
{ "$group": {
"_id": "$_id",
"id": { "$first": "$id" },
"name": { "$first": "$name" },
"starred": { "$first": "$starred" },
"subjects": { "$push": "$subjects.faculties.subjects" }
}}
])
Использование, конечно, этапа $redact
для фильтрации логического сравнения, поскольку для сравнения есть только $expr
с MongoDB 3.6 и $setIsSubset
в массив "starred"
.
Тогда, конечно, из-за всех операций $unwind
обычно требуется $group
, чтобы преобразовать массив.
Или иначе сделать $lookup
с другого направления:
db.schools.aggregate([
{ "$unwind": "$faculties" },
{ "$unwind": "$faculties.subjects" },
{ "$lookup": {
"from": "users",
"localField": "faculties.subjects.id",
"foreignField": "starred",
"as": "users"
}},
{ "$unwind": "$users" },
{ "$match": { "users.name": "Michael" } },
{ "$group": {
"_id": "$users._id",
"id": { "$first": "$users.id" },
"name": { "$first": "$users.name" },
"starred": { "$first": "$users.starred" },
"subjects": {
"$push": "$faculties.subjects"
}
}}
])
Последняя форма на самом деле не идеальна, так как вы не фильтруете «пользователей» до тех пор, пока не будет сделано $lookup
(или, технически говоря, «во время» $lookup
действительно ). Но, во всяком случае, в первую очередь он должен работать со всей коллекцией "школ".
Все формы возвращают одинаковый вывод:
{
"_id" : ObjectId("5aea649526a94676bb981df4"),
"id" : 1,
"name" : "Michael",
"starred" : [
1,
2
],
"subjects" : [
{
"id" : 1,
"name" : "sub1"
},
{
"id" : 2,
"name" : "sub2"
}
]
}
Если у вас есть только детали из внутреннего массива "subjects"
из соответствующего документа, которые фактически соответствуют значениям "starred"
для текущего пользователя.
Все это говорит о том, что не очень хорошая идея "вкладывать массивы" в MongoDB. До MongoDB 3.6 вы не можете даже атомарные обновления «вложенных массивов» и даже с изменениями, которые позволяют это, все равно «сложно» в лучшем случае выполнять любые операции запроса, и особенно те, которые связаны с объединениями и фильтрацией.
Распространенной ошибкой новичка является структурирование «вложенных массивов», поскольку вы, похоже, думаете, что «лучше» организовываете вещи. Но на самом деле это скорее «анти-паттерн», и вы действительно должны рассмотреть «более плоскую» структуру, такую как:
{
"_id" : ObjectId("5aea651326a94676bb981df5"),
"id" : 1,
"name" : "Uni",
"subjects" : [
{
"id" : 1,
"name" : "sub1",
"facultyId": 1000,
"facultyName": "faculty1"
},
{
"id" : 2,
"name" : "sub2",
"facultyId": 1000,
"facultyName": "faculty1"
},
{
"id" : 3,
"name" : "sub3",
"facultyId": 1000,
"facultyName": "faculty1"
}
]
}
Что "намного" легче работать и, конечно, выполнять "соединения" там, где это необходимо.