MongoDB nested lookup with 3 levels
With the mongodb 3.6 and above $lookup
syntax it is quite simple to join nested fields without using $unwind
.
db.party.aggregate([
{ "$lookup": {
"from": "address",
"let": { "partyId": "$_id" },
"pipeline": [
{ "$match": { "$expr": { "$eq": ["$party_id", "$$partyId"] }}},
{ "$lookup": {
"from": "addressComment",
"let": { "addressId": "$_id" },
"pipeline": [
{ "$match": { "$expr": { "$eq": ["$address_id", "$$addressId"] }}}
],
"as": "address"
}}
],
"as": "address"
}},
{ "$unwind": "$address" }
])
The cause of your 'troubles' is the second aggregation stage - { $unwind: "$address" }
. It removes record for party with _id: 4
(because its address array is empty, as you mention) and produces two records for parties _id: 1
and _id: 5
(because each of them has two addresses).
To prevent removing of parties without addresses you should set
preserveNullAndEmptyArrays
option of$unwind
stage totrue
.To prevent duplicating of parties for its different addresses you should add
$group
aggregation stage to your pipeline. Also, use$project
stage with$filter
operator to exclude empty address records in output.
db.party.aggregate([{
$lookup: {
from: "address",
localField: "_id",
foreignField: "party_id",
as: "address"
}
}, {
$unwind: {
path: "$address",
preserveNullAndEmptyArrays: true
}
}, {
$lookup: {
from: "addressComment",
localField: "address._id",
foreignField: "address_id",
as: "address.addressComment",
}
}, {
$group: {
_id : "$_id",
name: { $first: "$name" },
address: { $push: "$address" }
}
}, {
$project: {
_id: 1,
name: 1,
address: {
$filter: { input: "$address", as: "a", cond: { $ifNull: ["$$a._id", false] } }
}
}
}]);