Agregate query with lookup and restriction condition in MongoDb - mongodb

I have a collection myCollection
{
name : String,
members: [{status : Number, memberId : {type: Schema.Types.ObjectId, ref: 'members'}]
}
with this data :
"_id" : ObjectId("5e8b0bac041a913bc608d69d")
"members" : [
{
"status" : 4,
"_id" : ObjectId("5e8b0bac041a913bc608d69e"),
"memberId" : ObjectId("5e7dbf5b257e6b18a62f2da9"),
"date" : ISODate("2020-04-06T10:59:56.997Z")
},
{
"status" : 1,
"_id" : ObjectId("5e8b0bf2041a913bc608d6a3"),
"memberId" : ObjectId("5e7e2f048f80b46d786bfd67"),
"date" : ISODate("2020-04-06T11:01:06.463Z")
}
],
and a collection members
{
firstname : String
lastname : String
}
with this data :
[{
"_id" : ObjectId("5e7dbf5b257e6b18a62f2da9"),
"firstname" : "raed",
"lastname" : "besbes"
},
{
"_id" : ObjectId("5e7e2f048f80b46d786bfd67"),
"firstname" : "sarra",
"lastname" : "besbes"
}]
I make a query with aggregate and $lookup, to have the data populated but I want to restrict returned
data on status 1 only, this is my query and result.
how can I get data populated with only status 1 members returned ? Thank you.
query
db.getCollection('myCollection').aggregate([
{ $match: { _id: ObjectId("5e8b0bac041a913bc608d69d")}},
{
"$lookup": {
"from": "members",
"localField": "members.memberId",
"foreignField": "_id",
"as": "Members"
}
},
{
$project: {
"Members.firstname" : 1,
"Members.lastname" : 1,
"Members._id" : 1,
},
}
])
result
{
"_id" : ObjectId("5e8b0bac041a913bc608d69d"),
"Members" : [
{
"_id" : ObjectId("5e7dbf5b257e6b18a62f2da9"),
"firstname" : "raed",
"lastname" : "besbes"
},
{
"_id" : ObjectId("5e7e2f048f80b46d786bfd67"),
"firstname" : "sarra",
"lastname" : "besbes"
}
]
}

Option 1
Filter members before $lookup
db.myCollection.aggregate([
{
$match: {
_id: ObjectId("5e8b0bac041a913bc608d69d")
}
},
{
$addFields: {
members: {
$filter: {
input: "$members",
cond: {
$eq: [
"$$this.status",
1
]
}
}
}
}
},
{
"$lookup": {
"from": "members",
"localField": "members.memberId",
"foreignField": "_id",
"as": "Members"
}
},
{
$project: {
"Members.firstname": 1,
"Members.lastname": 1,
"Members._id": 1
}
}
])
MongoPlayground
Option 2
(Similar to 1) We flatten member array, filter only status = 1 and then perform $lookup.
db.myCollection.aggregate([
{
$match: {
_id: ObjectId("5e8b0bac041a913bc608d69d")
}
},
{
"$unwind": "$members"
},
{
$match: {
"members.status": 1
}
},
{
"$lookup": {
"from": "members",
"localField": "members.memberId",
"foreignField": "_id",
"as": "Members"
}
},
{
"$unwind": "$Members"
},
{
$group: {
_id: "$_id",
Members: {
$push: "$Members"
}
}
}
])
MongoPlayground
Option 3
We can apply filter for Member array based on filtered values for member array.
db.myCollection.aggregate([
{
$match: {
_id: ObjectId("5e8b0bac041a913bc608d69d")
}
},
{
"$lookup": {
"from": "members",
"localField": "members.memberId",
"foreignField": "_id",
"as": "Members"
}
},
{
$project: {
Members: {
$filter: {
input: "$Members",
cond: {
$in: [
"$$this._id",
{
$let: {
vars: {
input: {
$filter: {
input: "$members",
cond: {
$eq: [
"$$this.status",
1
]
}
}
}
},
in: "$$input.memberId"
}
}
]
}
}
}
}
}
])
MongoPlayground

Related

How to $lookup by avoiding null values in mongodb aggregate

In here i'm using $lookup to to a left join from other collections, the query works fine but when some records missing values it returns
errmsg : $in requires an array as a second argument, found: null
Heres the querying document structure :
{
"no" : "2020921008981",
"sale" : {
"soldItems" : [
{
"itemId" : "5b55ac7f0550de00210a3b24",
},
{
"itemId" : "5b55ac7f0550de00215584re",
}
],
"bills" : [
{
"billNo" : "2020921053467",
"insurancePlanId" : "160",
},
{
"billNo" : "2020921053467",
"insurancePlanId" : "170",
}
],
"visitIds" : [
5b55ac7f0550de00210a3b24, 5b55ac7f0550de00210a3b24
]
}
}
the query :
db.case.aggregate([
{
$lookup: {
from: "insurance",
let: { ipids: "$sale.bill.insurancePlanId" },
pipeline: [
{
$unwind: "$coveragePlans"
},
{
$match: { $expr: { $in: ["$coveragePlans._id", "$$ipids"] } }
},
{
$project: { _id: 0, name: 1 }
}
],
as: "insurances"
}
},
{
$lookup: {
from: "item",
let: { iid: "$salesOrder.purchaseItems.itemRefId" },
pipeline: [
{
$match: {
$expr: {
$in: ["$_id", {
$map: {
input: "$$iid",
in: { $toObjectId: "$$this" }
}
}
]
}
}
}
],
as: "items"
}
}
])
insurance collection :
{
"_id" : ObjectId("5b55aca20550de00210a6d25"),
"name" : "HIJKL"
"coveragePlans" : [
{
"_id" : "160",
"name" : "UVWZ",
},
{
"_id" : "161",
"name" : "LMNO",
}
]
},
{
"_id" : ObjectId("5b55aca20550de00210a6d25"),
"name" : "WXYZ"
"coveragePlans" : [
{
"_id" : "169",
"name" : "5ABC",
},
{
"_id" : "170",
"name" : "4XYZ",
}
]
}
item collection :
{
"_id" : ObjectId("5b55ac7f0550de00210a3b24"),
"code" : "ABCDE"
},
{
"_id" : ObjectId("5b55ac7f0550de00215584re"),
"code" : "PQRST"
}
How to avoid this and do null checks effectively before pipe-lining into the next stages? Tried with { $match: { "fieldName": { $exists: true, $ne: null } } } but it returns mongo error regarding the format. If its the way to go please mention the stage i should put that.. Thanks in advance
You can use $ifNull operator
let: { ipids: {$ifNull:["$sale.bill.insurancePlanId", [] ]} },
EDIT: To skip empty "$salesOrder.purchaseItems.itemRefId" values
let: { iid: {$filter: {input:"$salesOrder.purchaseItems.itemRefId", cond:{$ne:["$$this", ""]}}} },
You can get around that by not using $in.
It looks like this $map is executed separately for every document in the items collection. If you were to run the map in an $addFields stage, you could used the simple form of lookup to match the added field to _id, which would automagically handle missing, null, and array.
Remove the added field with a $project stage if necessary.
db.case.aggregate([
{$lookup: {
from: "insurance",
let: { ipids: "$sale.bill.insurancePlanId" },
pipeline: [
{$unwind: "$coveragePlans"},
{$match: { $expr: { $in: ["$coveragePlans._id", "$$ipids"] } }},
{$project: { _id: 0, name: 1 }}
],
as: "insurances"
}}
{$addFields:{
matchArray:{$map: {
input: "$$iid",
in: { $toObjectId: "$$this" }
}}
}},
{$lookup: {
from: "item",
localField: "matchArray",
foreignField:"_id",
as: "items"
}},
{$project:{
arrayField: 0
}}
])

Self join query in mongodb and return fields from parent and child documents with condition

Suppose I have multiple documents like these in a collection
Parent document:
{
"_id" : ObjectId("5e86ebd6c2d28863e4e2c920"),
"users" : [],
"name" : "Annual",
"days" : 18,
},
Child documents:
{
"_id" : ObjectId("5e86ec22c2d28863e4e2c921"),
"users" : [
ObjectId("5e58fa20f3bea73c3cb07713"),
ObjectId("5e58fab5f3bea73c3cb07715")
],
"leaveTypeId" : ObjectId("5e86ebd6c2d28863e4e2c920"),
"name" : "Personal",
"days" : 5,
},
{
"_id" : ObjectId("5e86ec22c2d28863e4e2c921"),
"users" : [],
"leaveTypeId" : ObjectId("5e86ebd6c2d28863e4e2c920"),
"name" : "Personal",
"days" : 5,
}
Now I want to build a query like if user found in users array then return name and days from child document otherwise it should return name and days from parent document.
If user_id = ObjectId("5e58fa20f3bea73c3cb07713") then the output should be
{
name: 'Personal',
days: 5
}
If user_id = ObjectId("52fff32rax823vnvy3234es12") then the output should be
{
name: 'Annual',
days: 18
}
Try these aggregation queries :
When it's done on Parent Collection :
db.parent.aggregate([
{
$lookup: {
from: "child",
localField: "_id",
foreignField: "leaveTypeId",
as: "child_docs"
}
},
{
$unwind: "$child_docs"
},
{
$project: {
name: {
$cond: [
{ $in: [ObjectId("5e58fa20f3bea73c3cb07713"), "$child_docs.users"] },
"$child_docs.name",
"$name"
]
},
days: {
$cond: [
{ $in: [ObjectId("5e58fa20f3bea73c3cb07713"), "$child_docs.users"] },
"$child_docs.days",
"$days"
]
}
}
}
]);
Test : MongoDB-Playground
When it's done on Child Collection :
db.child.aggregate([
{
$lookup: {
from: "parent",
localField: "leaveTypeId",
foreignField: "_id",
as: "parent_docs"
}
},
{
$unwind: "$parent_docs"
},
{
$project: {
name: {
$cond: [
{ $in: [ObjectId("5e58fa20f3bea73c3cb07713"), "$users"] },
"$name",
"$parent_docs.name"
]
},
days: {
$cond: [
{ $in: [ObjectId("5e58fa20f3bea73c3cb07713"), "$users"] },
"$days",
"$parent_docs.days"
]
}
}
}
]);
Test : MongoDB-Playground

How to aggregate array of ObjectId pairs with their relevant collection

I have a course collection in which I am allotting teachers for each subject of that course. The allotment is saved as an array of JSON please take a look at the reference doc below.
{
"_id" : ObjectId("5cc7d72d8e165005cbef939e"),
"isAssigned" : true,
"name" : "11",
"section" : "A",
"allotment" : [
{
"subject" : ObjectId("5cc3f7cc88e95a0c8e8ccd7d"),
"teacher" : ObjectId("5cbee0e37a3c852868ec9797")
},
{
"subject" : ObjectId("5cc3f80e88e95a0c8e8ccd7e"),
"teacher" : ObjectId("5cbee10c7a3c852868ec9798")
}
]
}
I am trying to match the subject and teacher fields along with their doc from two different collections. I could get them in two different array's but couldn't get them as structured in my expected output
Doc in teachers collection
{
_id: ObjectId("5cbee0e37a3c852868ec9797"),
name: "Alister"
}
Doc in subject
{
_id: ObjectId("5cc3f7cc88e95a0c8e8ccd7d"),
name: "English",
code: "EN"
}
Query I tried
Course.aggregate([
{"$match": matchQuery},
{"$lookup": {
"from": "subjects",
"localField": "allotment.subject",
"foreignField": "_id",
"as": "subjectInfo"
}
},
{"$lookup": {
"from": "teachers",
"localField": "allotment.teacher",
"foreignField": "_id",
"as": "teacherInfo"}
},
])
Output of that Query
{
isAssigned: true
name: "11"
section: "A"
subjectInfo:[
{_id: "5cc3f7cc88e95a0c8e8ccd7d", name:"English", code:"EN"}
{_id: "5cc3f80e88e95a0c8e8ccd7e", name: "Science", code:"SC"}
]
teacherInfo:[
{_id: ObjectId("5cbee0e37a3c852868ec9797"),name: "Alister"},
{ _id: ObjectId("5cbee10c7a3c852868ec9798"),name: "Frank"}
]
}
Expexted output
{
"_id" : ObjectId("5cc7d72d8e165005cbef939e"),
"isAssigned" : true,
"name" : "11",
"section" : "A",
"allotment" : [
{
"subject" : {
_id: ObjectId("5cc3f7cc88e95a0c8e8ccd7d"),
name: "English",
code: "EN"
}
"teacher" : {
_id: ObjectId("5cbee0e37a3c852868ec9797"),
name: "Alister"
}
},
{
"subject" : {
_id: ObjectId("5cc3f80e88e95a0c8e8ccd7e"),
name: "Science",
code: "SC"
}
"teacher" : {
_id: ObjectId("5cbee10c7a3c852868ec9798"),
name: "Frank"
}
}
]
}
Just unwind the array before the lookups:
Course.aggregate([
{"$match": matchQuery},
{"$unwind: "$allotment"}
{"$lookup": {
"from": "subjects",
"localField": "allotment.subject",
"foreignField": "_id",
"as": "subjectInfo"
}
},
{"$lookup": {
"from": "teachers",
"localField": "allotment.teacher",
"foreignField": "_id",
"as": "teacherInfo"}
},
])
if you want to re-group after that to restore expected format you can add:
{ $group : {
_id: "$_id",
name: {$first: "$name"},
section: {$first: "$section},
isAssigned: {$first: "$isAssigned},
allotment: {$push: {teacher: "$teacherInfo.0", subject: "$subjectInfo.0"}}
I'm assuming teacherInfo and subjectInfo are never empty, if this is not the case you should add a $match to filter empty ones.
Take a look at $lookup aggregation stage which lets you join collections. There's a plenty of examples on the usage in the documentation.
EDIT: Here's the complete pipeline that should provide the expected result:
courses.aggregate(
[
{
"$unwind" : {
"path" : "$allotment"
}
},
{
"$lookup" : {
"from" : "subjects",
"localField" : "allotment.subject",
"foreignField" : "_id",
"as" : "allotment.subject"
}
},
{
"$lookup" : {
"from" : "teachers",
"localField" : "allotment.teacher",
"foreignField" : "_id",
"as" : "allotment.teacher"
}
},
{
"$addFields" : {
"allotment.subject" : {
"$arrayElemAt" : [
"$allotment.subject",
0.0
]
},
"allotment.teacher" : {
"$arrayElemAt" : [
"$allotment.teacher",
0.0
]
}
}
},
{
"$group" : {
"_id" : "$_id",
"isAssigned" : {
"$first" : "$isAssigned"
},
"name" : {
"$first" : "$name"
},
"section" : {
"$first" : "$section"
},
"allotment" : {
"$addToSet" : "$allotment"
}
}
}
]
)
Firstly you have to $unwind the allotment array and then apply $lookup for subject and then repeat same for teachers and finally apply $group to combine back it inside array. See below aggregate query that is have tried and its working for me.
Course.aggregate([
{"$match": matchQuery},
{
$unwind: '$allotment'
},
{
$lookup:{
"from": "subjects",
"localField": "allotment.subject",
"foreignField": "_id",
"as": "allotment.subject"
}
},
{
$unwind: '$allotment.subject'
},
{
"$lookup": {
"from": "teachers",
"localField": "allotment.teacher",
"foreignField": "_id",
"as": "allotment.teacher"
}
},
{
$unwind: '$allotment.teacher'
},
{
"$group" : {
"_id" : "$_id",
"isAssigned" : {
"$first" : "$isAssigned"
},
"name" : {
"$first" : "$name"
},
"section" : {
"$first" : "$section"
},
"allotment" : {
"$addToSet" : "$allotment"
}
}
}
])

Mongodb aggretate apply sort to lookup results, and add field index number

The aggregate was executed.
I got the results using lookup, but I need a sort.
In addition, I want to assign an index to the result value.
CollectionA :
{
"_id" : ObjectId("5a6cf47415621604942386cd"),
"contents" : [
ObjectId("AAAAAAAAAAAAAAAAAAAAAAAA"),
ObjectId("BBBBBBBBBBBBBBBBBBBBBBBB")
],
"name" : "jason"
}
CollectionB :
{
"_id" : ObjectId("AAAAAAAAAAAAAAAAAAAAAAAA")
"title" : "a title",
"date" : 2018-01-02
},
{
"_id" : ObjectId("BBBBBBBBBBBBBBBBBBBBBBBB")
"title" : "a title",
"date" : 2018-01-01
}
Query:
db.getCollection('A').aggregate([
{
$match : { "_id" : ObjectId("5a6cf47415621604942386cd") }
},
{
$lookup : {
from: "B",
localField: "contents",
foreignField: "_id",
as: "item"
}
},
{ $sort: { "item.date" : -1 } }
]);
Want Result:
{
"_id" : ObjectId("5a6cf47415621604942386cd"),
"contents" : [
{
"_id" : ObjectId("BBBBBBBBBBBBBBBBBBBBBBBB")
"title" : "a title",
"date" : 2018-01-01,
"index" : 0
},
{
"_id" : ObjectId("AAAAAAAAAAAAAAAAAAAAAAAA")
"title" : "a title",
"date" : 2018-01-02,
"index" : 1
}],
"name" : "jason"
}
The current problem does not apply to the sort.
And I don't know how to designate an index.
Below Aggregation may you. For your desire result.
db.CollectionA.aggregate([
{
$match: { "_id": ObjectId("5a6cf47415621604942386cd") }
},
{
$lookup: {
from: "CollectionB",
let: { contents: "$contents" },
pipeline: [
{
$match: { $expr: { $in: ["$_id", "$$contents"] } }
},
{ $sort: { date: 1 } }
],
as: "contents"
}
},
{
$project: {
contents: {
$map: {
input: { $range: [0, { $size: "$contents" }, 1 ] },
as: "element",
in: {
$mergeObjects: [
{ index: "$$element" },
{ $arrayElemAt: [ "$contents", "$$element" ]}
]
}
}
}
}
}
])
One way to go about it would be to unwind the array, sort it and then group it back
db.A.aggregate([
{
$match: {
"_id": ObjectId("5a6cf47415621604942386cd")
}
},
{
$lookup: {
from: "B",
localField: "contents",
foreignField: "_id",
as: "item"
}
},
{
$unwind: "$item"
},
{
$sort: {
"item.date": -1
}
},
{
$group: {
_id: "$_id",
contents: {
$push: "$item"
}
}
}
])
Another method is, (this is applicable only if the date field corresponds to the document creation date),
db.A.aggregate([
{
$match: {
"_id": ObjectId("5a6cf47415621604942386cd")
}
},
{
$lookup: {
from: "B",
localField: "contents",
foreignField: "_id",
as: "item"
}
},
{
$sort: {
"item": -1
}
}
])
Basically, this sorts on the basis of _id, and since _id is created using the creation date, it should sort accordingly.

$lookup with nested data in mongodb

How do I combine 2 array objects using mongoDB NoSQL? Because I have tried to find some of the same problems here that I got, but I have not found the answers and problems that match what I got.
If someone here wants to help me, here are the problems I want to solve.
Example: I tried using noSQL in mongoDB like this:
db.tables.aggregate([
{ $lookup: { from: 'reservations', localField: '_id', foreignField: 'tableId', as: 'reservation' }},
{ $unwind: { path: '$reservation', 'preserveNullAndEmptyArrays': true }},
{ $lookup: { from: 'orders', localField: 'reservation._id', foreignField: 'reservationId', as: 'orders' }},
{ $lookup: { from: 'products', localField: 'orders.productId', foreignField: '_id', as: 'products' }},
{
$project: {
'_id': 1,
'initial': 1,
'description': 1,
'reservation._id': 1,
'reservation.guest': 1,
'orders._id': 1,
'orders.status': 1,
'orders.quantity': 1,
'orders.productId': 1,
'products._id': 1,
'products.name': 1
}
},
]);
After running noSQL mongoDB above, I got the results below:
{
"_id" : ObjectId("5b63e519514cf01c2864749a"),
"description" : "Kursi VIP 01",
"reservation" : {
"_id" : ObjectId("5b63f104514cf01c286474b6"),
"guest" : "Jhon Doe"
},
"orders" : [
{
"_id" : ObjectId("5b63f239514cf01c286474bb"),
"productId" : ObjectId("5b63e72d514cf01c286474a3"),
"status" : "3",
"quantity" : "2"
},
{
"_id" : ObjectId("5b63f252514cf01c286474bc"),
"productId" : ObjectId("5b63e7de514cf01c286474a6"),
"status" : "2",
"quantity" : "3"
},
{
"_id" : ObjectId("5b63f267514cf01c286474bd"),
"productId" : ObjectId("5b63e937514cf01c286474ac"),
"status" : "0",
"quantity" : "2"
}
],
"products" : [
{
"_id" : ObjectId("5b63e72d514cf01c286474a3"),
"name" : "AQUA 600ML"
},
{
"_id" : ObjectId("5b63e7de514cf01c286474a6"),
"name" : "Nasi Goreng Kecap Asin"
},
{
"_id" : ObjectId("5b63e937514cf01c286474ac"),
"name" : "Daging Ayam Goreng"
}
]
}
Now, my Question is. How to merge/combine 2 Object Array ("orders and products"), So I can get results like this:
{
"_id" : ObjectId("5b63e519514cf01c2864749a"),
"description" : "Kursi VIP 01",
"reservation" : {
"_id" : ObjectId("5b63f104514cf01c286474b6"),
"guest" : "Jhon Doe"
},
"orders" : [
{
"_id" : ObjectId("5b63f239514cf01c286474bb"),
"productId" : ObjectId("5b63e72d514cf01c286474a3"),
"name" : "AQUA 600ML",
"status" : "3",
"quantity" : "2"
},
{
"_id" : ObjectId("5b63f252514cf01c286474bc"),
"productId" : ObjectId("5b63e7de514cf01c286474a6"),
"name" : "Nasi Goreng Kecap Asin",
"status" : "2",
"quantity" : "3"
},
{
"_id" : ObjectId("5b63f267514cf01c286474bd"),
"productId" : ObjectId("5b63e937514cf01c286474ac"),
"name" : "Daging Ayam Goreng"
"status" : "0",
"quantity" : "2"
}
]
}
I hope, someone can help me.
Thanks in advance.
You can try below aggregation with mongodb 3.4
You need to $unwind the orders array to add the field($addFields) name inside orders and then $group to rollback orders again to the make an array field
db.tables.aggregate([
{ "$lookup": {
"from": "reservations",
"localField": "_id",
"foreignField": "tableId",
"as": "reservation"
}},
{ "$unwind": { "path": '$reservation', 'preserveNullAndEmptyArrays': true }},
{ "$lookup": {
"from": "orders",
"localField": "reservation._id",
"foreignField": "reservationId",
"as": "orders",
}},
{ "$unwind": { "path": '$orders', 'preserveNullAndEmptyArrays': true }},
{ "$lookup": {
"from": "products",
"localField": "orders.productId",
"foreignField": "_id",
"as": "orders.products"
}},
{ "$unwind": { "path": '$orders.products', 'preserveNullAndEmptyArrays': true }},
{ "$addFields": {
"orders.name": "$orders.products.name"
}},
{ "$group": {
"_id": "$_id",
"description": { "$first": "$description" },
"reservation": { "$first": "$reservation" },
"orders": { "$push": "$orders" }
}},
{ "$project": { "orders.products": 0 }}
])
Which is far simple with mongodb 3.6 nested $lookup version
db.tables.aggregate([
{ "$lookup": {
"from": "reservations",
"let": { "reservationId": "$_id" },
"pipeline": [
{ "$match": { "$expr": { "$eq": [ "$tableId", "$$reservationId" ] } } }
],
"as": "reservations"
}},
{ "$lookup": {
"from": "orders",
"let": { "reservationId": "$reservation._id" },
"pipeline": [
{ "$match": { "$expr": { "$eq": [ "$reservationId", "$$reservationId" ] } } },
{ "$lookup": {
"from": "products",
"let": { "productId": "$productId" },
"pipeline": [
{ "$match": { "$expr": { "$eq": [ "$_id", "$$productId" ] } } },
{ "$project": { "_id": false }}
],
"as": "products"
}},
{ "$unwind": "$products" },
{ "$addFields": { "name": "$products.name" } },
{ "$project": { "products": 0 }}
],
"as": "orders"
}}
])