I'm trying to implement a query that will merge two collections into one and this can be done using $lookup. What I'm trying to achieve is that if the localField doesn't match foreignField I wanted that record to be included as well. Is this possible or am I missing something? Below is what I've tried so far which is a basic $lookup aggregation:
const billPayments = await BillPayment.aggregate([
{
$match: {
isActive: true,
},
},
{
$lookup: {
from: PackageList.collection.name,
localField: "serviceNumber",
foreignField: "serviceNumber",
as: "package",
},
},
{ $unwind: "$package" },
{
$project: {
serviceNumber: 1,
amount: 1,
isCaution: 1,
paymentDate: 1,
remark: 1,
package: "$package",
id: "$_id",
},
},
]);
res.json(billPayments);
The current result of billPayments is only for those BillPayment data which serviceNumber exists in both collections. But I wanted those records which don't exist in PackageList to be included as well. How can I achieve that?
I'm pretty sure I'm missing something here. Any help is appreciated.
#Edit for expected output
"billPayments": [
{
"amount": 299.25,
"id": "604f5bbd6bf57e1f9c58203f",
"isActive": true,
"isCaution": false,
"packageService": {
"_id": "5fc4a5e15ce5bf227451f4b8",
"isActive": true,
"serviceType": "Post_Paid",
"moneyLimit": 3000
},
"employee": {
// Employee data from Employee collection to be included
}
"paymentDate": "2021-03-14T21:00:00.000Z",
"remark": "",
"serviceNumber": "0935998681"
},
{
"amount": 299.25,
"id": "604f5bbd6bf57e1f9c58203f",
"isActive": true,
"packageService": {
// If there is no match for serviceNumber
},
"paymentDate": "2021-03-14T21:00:00.000Z",
"remark": "",
"serviceNumber": "0735448621"
}
]
Edit for additional $lookup query:
I have got a second level $lookup to get additional information from another collection. Which is like below:
{
$lookup: {
from: PackageList.collection.name,
localField: "serviceNumber",
foreignField: "serviceNumber",
as: "package",
},
},
{ $unwind: "$package" },
{
$lookup: {
from: Employee.collection.name,
localField: "package.employee",
foreignField: "_id",
as: "employee",
},
},
As #DheemanthBhat suggested in his answer, I've tried to remove unwind from Package and can easily get an empty list. But if I tried to $lookup on Package to get the employee information, it throws MongoError since it cannot lookup on an empty object.
Is there any method to skip those empty packages from a second level $lookup? Or how can I use $ifNull in this case to skip $unwind from empty results?
So when you perform $lookup without $unwind all the details from BillPayment collection matched or unmatched with the PackageList collection will be available. If you $unwind then the documents with empty packageService will be removed. Based on your expected output If you want an empty packageService object then check the below query.
Note: Please verify the collection/model name and field names before using the below code.
const billPayments = await BillPayment.aggregate([
{
$match: { isActive: true }
},
{
$lookup: {
from: "PackageList",
localField: "serviceNumber",
foreignField: "serviceNumber",
as: "packageService"
}
},
{
$project: {
serviceNumber: 1,
amount: 1,
isCaution: 1,
paymentDate: 1,
remark: 1,
package: "$package",
id: "$_id",
packageService: {
$ifNull: [{ $arrayElemAt: ["$packageService", 0] }, {}]
}
}
}
]);
Output:
/* 1 createdAt:3/15/2021, 6:36:05 PM*/
{
"_id" : ObjectId("604f5bbd6bf57e1f9c58203f"),
"isCaution" : false,
"amount" : 299.25,
"paymentDate" : ISODate("2021-03-15T02:30:00.000+05:30"),
"serviceNumber" : "0935998681",
"id" : ObjectId("604f5bbd6bf57e1f9c58203f"),
"packageService" : {
"_id" : ObjectId("5fc4a5e15ce5bf227451f4b8"),
"serviceNumber" : "0935998681",
"isActive" : true,
"serviceType" : "Post_Paid",
"moneyLimit" : 3000
}
},
/* 2 createdAt:3/15/2021, 6:36:05 PM*/
{
"_id" : ObjectId("604f5bbd6bf57e1f9c582040"),
"isCaution" : false,
"amount" : 300,
"paymentDate" : ISODate("2021-03-16T02:30:00.000+05:30"),
"serviceNumber" : "0735448621",
"id" : ObjectId("604f5bbd6bf57e1f9c582040"),
"packageService" : {
}
}
Test data:
BillPayment collection
{
"_id" : ObjectId("604f5bbd6bf57e1f9c58203f"),
"isActive" : true,
"isCaution" : false,
"amount" : 299.25,
"paymentDate" : ISODate("2021-03-15T02:30:00.000+05:30"),
"serviceNumber" : "0935998681"
},
{
"_id" : ObjectId("604f5bbd6bf57e1f9c582040"),
"isActive" : true,
"isCaution" : false,
"amount" : 300,
"paymentDate" : ISODate("2021-03-16T02:30:00.000+05:30"),
"serviceNumber" : "0735448621"
}
PackageList collection
{
"_id" : ObjectId("5fc4a5e15ce5bf227451f4b8"),
"serviceNumber" : "0935998681",
"isActive" : true,
"serviceType" : "Post_Paid",
"moneyLimit" : 3000
}
Related
I want to populate just those documents from users collection where users._id is equal to levelOne.assignedPeople ids. This will arrive as an array.
I am using this query,
db.parentepics.aggregate([
{
$match: {
ideaId: "616bffd81a9c715679374b19",
isDeleted: false,
isHidden: false,
isRemovedByOwner: false,
},
},
{
$lookup: {
from: "childtasklevelones",
localField: "_id",
foreignField: "parentEpicId",
as: "level_1",
},
},
{
$match: {
"level_1.assignedPeople": ObjectId('616c00001a9c715679374b1b'),
},
},
{
$project: {
_id: 1,
epicName: 1,
epicDescription: 1,
epicPriority: 1,
startsFrom: 1,
endsAt: 1,
levelOne: "$level_1",
},
}
])
Which produces some output looks like this,
{
"_id" : ObjectId("616c099e1a9c715679374b7c"),
"epicName" : "Marketing",
"epicDescription" : "Marketing data",
"epicPriority" : "1",
"startsFrom" : ISODate("2021-10-15T14:36:31.387+05:30"),
"endsAt" : ISODate("2021-10-30T14:36:31.387+05:30"),
"levelOne" : [
{
"_id" : ObjectId("616db350123f016231912954"),
"currentStatus" : "pending",
"assignedPeople" : [
ObjectId("616c00001a9c715679374b1b")
],
"isRemovedByOwner" : false,
"highlightColorPreference" : "yellow",
"isHidden" : false,
"isDeleted" : false,
"taskName" : "Marketing",
"taskDescription" : "Marketing data",
"taskPriority" : "1",
"startsFrom" : ISODate("2021-10-15T14:36:31.387+05:30"),
"endsAt" : ISODate("2021-10-30T14:36:31.387+05:30"),
"createdBy" : ObjectId("616bffd81a9c715679374b18"),
"parentEpicId" : ObjectId("616c099e1a9c715679374b7c"),
"taskNameToLower" : "marketing",
"createdAt" : ISODate("2021-10-18T23:18:00.464+05:30"),
"updatedAt" : ISODate("2021-10-18T23:18:00.464+05:30"),
"__v" : 0
}
]
}
Now I just need to populate users from users collection where users._id is equal to levelOne.assignedPeople.
I sort, I need the array of users in place of these levelOne.assignedPeople ids.
Please help me to modify the query.
Thanks
You just need to add an extra lookup stage. In this case, because level_1.assignedPeople is already an array, we can just use the lookup stage to replace the array of User Object IDs with an array of User Objects.
Example:
db.parentepics.aggregate([
{ $match: {
ideaId: "616bffd81a9c715679374b19",
isDeleted: false,
isHidden: false,
isRemovedByOwner: false,
} },
{ $lookup: {
from: "childtasklevelones",
localField: "_id",
foreignField: "parentEpicId",
as: "level_1",
} },
{ $match: {
"level_1.assignedPeople": ObjectId('616c00001a9c715679374b1b'),
} },
{ $lookup: {
from: "users",
localField: "level_1.assignedPeople",
foreignField: "_id",
as: "level_1.assignedPeople",
} },
{ $project: {
_id: 1,
epicName: 1,
epicDescription: 1,
epicPriority: 1,
startsFrom: 1,
endsAt: 1,
levelOne: "$level_1",
} }
])
I have two collections events & members :
events Schema :
{
name : String,
members: [{status : Number, memberId : {type: Schema.Types.ObjectId, ref: 'members'}]
}
events Sample Doc :
"_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")
}
],
members Schema :
{
firstname : String
photo : String
}
members Sample Doc :
[{
"_id" : ObjectId("5e7dbf5b257e6b18a62f2da9"),
"firstname" : "raed",
"photo" : "/users/5e7dbf5b257e6b18a62f2da9/profile/profile-02b13aef6e.png"
},
{
"_id" : ObjectId("5e7e2f048f80b46d786bfd67"),
"firstname" : "sarra",
"photo" : "/5e7e2f048f80b46d786bfd67/profile/profile-c79f91aa2e.png"
}]
I made a query with aggregate, and lookup to get populated data of members, and I want to concat the photo fields of the members by a string, but I get an error,
How can I do the concat ?
Query :
db.getCollection('events').aggregate([
{ $match: { _id: ObjectId("5e8b0bac041a913bc608d69d")}},
{
"$lookup": {
"from": "members",
"localField": "members.memberId",
"foreignField": "_id",
"as": "Members"
}
},
{
$project: {
"Members.firstname" : 1,
"Members.photo": 1,
//"Members.photo": {$concat:["http://myurl", "$Members.photo"]},
"Members._id" : 1,
},
}
])
Result without the concat :
{
"_id" : ObjectId("5e8b0bac041a913bc608d69d"),
"Members" : [
{
"_id" : ObjectId("5e7dbf5b257e6b18a62f2da9"),
"firstname" : "raed",
"photo" : "/users/5e7dbf5b257e6b18a62f2da9/profile/profile-02b13aef6e.png"
},
{
"_id" : ObjectId("5e7e2f048f80b46d786bfd67"),
"firstname" : "sarra",
"photo" : "/5e7e2f048f80b46d786bfd67/profile/profile-c79f91aa2e.png"
}
]
}
Error :
$concat only supports strings, not array
You can do that simply by adding pipeline to $lookup stage
db.events.aggregate([
{
$match: {
_id: ObjectId("5e8b0bac041a913bc608d69d"),
},
},
{
$lookup: {
from: "members",
let: { memberId: "$members.memberId" },
pipeline: [
{ $match: { $expr: { $in: ["$_id", "$$memberId"] } } },
{
$project: {
firstname: 1,
photo: { $concat: ["http://myurl", "$photo"] }
}
}
],
as: "Members",
}
},
/** Optional */
{$project : {Members: 1}}
]);
Test : MongoDB-Playground
the alternative of using a pipeline in the above answer
we may use project and group
db.events.aggregate([
{
$match: { _id: ObjectId("5e8b0bac041a913bc608d69d") }
},
{
$unwind: '$members' // to spread the members array into a stream of documents
},
{
$lookup: {
from: "members",
localField: "members.memberId",
foreignField: "_id",
as: "member"
}
},
{
$unwind: '$member' // each document will have array of only one member, so do this unwind to convert it to an object
},
{
$project: { // do the project here to be able to use the $concat operator
'member._id': 1,
'member.firstname': 1,
'member.photo': 1,
'member.photo': { $concat: ['http://myurl', '$member.photo'] } // now we can use the $concat as member is an object, then member.photo exists
}
},
{
$group: { // do that grouping stage to gather all the members belong to the same document in one array again
_id: '$_id',
Members: {
$addToSet: '$member'
}
}
}
])
I have tried to solve this one but its WAY over my Mongo skill level.
I hope there are some hardcore Mongo wizards who have an idea :-)
I would like to make a result where
db.getCollection('invoice').find({
dueDate: {
$gte:148000000,
$lt: 149000000
}
})
This is the "invoice" table....
invoice
{
"_id" : "KLKIU",
"invoiceNumber" : 1,
"bookingId" : "0J0DR",
"dueDate" : "148100000",
"account" : "aaaaaaaaaa",
"invoiceLines" : [
{
"lineText" : "Booking fee",
"amount" : 1000
},
{
"lineText" : "Discount",
"amount" : -200
},
{
"lineText" : "Whatever extra",
"amount" : 400
}
]
}
this is the result
{
"_id" : "KLKIU",
"invoiceNumber" : 1,
"bookingId" : "0J0DR",
"dueDate" : "148100000",
"account" : "aaaaaaaaaa",
"invoiceLines" : [
{
"lineText" : "Booking fee",
"amount" : 1000
},
{
"lineText" : "Discount",
"amount" : -200
},
{
"lineText" : "Whatever extra",
"amount" : 400
}
],
"propertyName" : "Atlantis Condo",
}
please notice the "propertyName" at the bottom
it needs to lookup and add
"propertyName" : "Atlantis Condo",
which will be done like this
db.getCollection('booking').find({
booking._id: invoice.bookingId
})
and then
db.getCollection('property').find({
property._id: booking:propertyId
})
These are the two tables:
Booking
{
"_id" : "0J0DR",
"propertyId" : "58669471869659d70b424ea7",
}
Property
{
"_id" : "58669471869659d70b424ea7",
"propertyName" : "Atlantis Condo",
}
Hope someone can figure this out - right now im doing some horrible sequential loops, and with big amounts of data thats really slow.
You can try below aggregation.
$lookup's to join to Booking and Property collection.
$unwind to flatten the booking array output from $lookup for joining on local field to Property collection.
$addFields to project the propertyName field.
$project to exclude the fields from referenced collection.
db.getCollection('invoice').aggregate([{
$match: {
"dueDate": {
$gte: 148000000,
$lt: 149000000
}
}
}, {
$lookup: {
from: "Booking",
localField: "bookingId",
foreignField: "_id",
as: "booking"
}
}, {
$unwind: "$booking"
}, {
$lookup: {
from: "Property",
localField: "booking.propertyId",
foreignField: "_id",
as: "property"
}
}, {
$unwind: "$property"
}, {
$addFields: {
"propertyName": "$property.propertyName"
}
}, {
$project: {
"booking": 0
}
}, {
$project: {
"property": 0
}
}])
Here is my code
AbcSchema.aggregate([
{ $match: query },
{
$lookup: { from: 'xyz', localField: '_id', foreignField: 'place_id', as: 'xyzArray' }
}
])
Right now Im getting this result :
{
_id : "abc1",
abcfield1 : "...",
abcfield2 : "...",
xyzArray : [{_id : "xyz1", place_id : "abc1", xyzfield1 : "..."},
{_id : "xyz2", place_id : "abc1", xyzfield1 : "..."},
{_id : "xyz3", place_id : "abc1", xyzfield1 : "..."},
...] //all matching results
}
So now lets say I want only 2 documents in xyzArray, then how can I achieve that?
My requirement is to get limit the 'xyzArray' length to 'n' .
Here is the dynamic query. You can change the number inside the $slice to get the required number of array elements.
db.AbcSchema.aggregate([
{ $match: {_id : "abc1"} },
{
$unwind: "$_id"
},
{
$lookup: { from: 'xyz', localField: '_id', foreignField: 'place_id', as: 'xyzArray' }
},
{ $project: { abcfield1 : 1,
abcfield2 : 1,
xyzArrayArray: { $slice: ["$xyzArray", 2] }
}
}
]).pretty();
One possible solution using $project and $arrayElemAt
db.AbcSchema.aggregate([
{ $match: {_id : "abc1"} },
{
$unwind: "$_id"
},
{
$lookup: { from: 'xyz', localField: '_id', foreignField: 'place_id', as: 'xyzArray' }
},
{ $project: { abcfield1 : 1,
abcfield2 : 1,
firstxyzArray: { $arrayElemAt: [ "$xyzArray", 0 ] },
secondxyzArray: { $arrayElemAt: [ "$xyzArray", 1 ] }
}
}
]).pretty();
Sample Output:-
{
"_id" : "abc1",
"abcfield1" : "11",
"abcfield2" : "22",
"firstxyzArray" : {
"_id" : "xyz1",
"place_id" : "abc1",
"xyzfield1" : "xyzf1"
},
"secondxyzArray" : {
"_id" : "xyz2",
"place_id" : "abc1",
"xyzfield1" : "xyzf1"
}
}
I have two collections: one is items and the second one is user_item_history. I want to fetch items with their status. Status of each item is stored in user_item_history, and other details of the item are in the items collection. we have to filter data for particular user and category of item. so user_id and category is in user_item_history collection.
user_item_history:
{
"_id" : NumberLong(25424),
"_class" : "com.samepinch.domain.registration.UserItemHistory",
"user_id" : NumberLong(25416),
"item_id" : NumberLong(26220),
"catagoryPreference" : "BOTH",
"preference" : 0.6546536707079772,
"catagory" : "FOOD",
"status" : 1,
"createdDate" : ISODate("2015-09-02T07:50:36.760Z"),
"updatedDate" : ISODate("2015-09-02T07:55:24.105Z")
}
items:
{
"_id" : NumberLong(26220),
"_class" : "com.samepinch.domain.item.Item",
"itemName" : "Shoes",
"categoryName" : "SHOPPING",
"attributes" : [
"WESTERN",
"CASUAL",
"ELEGANT",
"LATEST"
],
"isAccessed" : false,
"imageUrl" : "0bd2838e-9349-432a-a200-6e6b659e853eitemcompressed.jpg",
"catagoryPreference" : "FEMALE",
"startDate" : ISODate("2015-11-26T18:30:00Z"),
"endDate" : ISODate("2015-11-27T18:30:00Z"),
"location" : {
"coordinates" : [
77.24149558372778,
28.56973445677584
],
"type" : "Point",
"radius" : 2000
},
"createdDate" : ISODate("2015-11-16T10:49:11.858Z"),
"updatedDate" : ISODate("2015-11-16T10:49:11.858Z")
}
As the final result, I would like to have documents of this format:
{
item_id:26220,
status:1,
imageUrl: "0bd2838e-9349-432a-a200-6e6b659e853eitemcompressed.jpg"
}
Update to MongoDB 3.2 and you'll be able to use the $lookup aggregation stage, which works similarly to SQL joins.
One-to-many relationship
If there are many corresponding user_item_history documents for each items document, you can get a list of item statuses as an array.
Query
db.items.aggregate([
{
$lookup:
{
from: "user_item_history",
localField: "_id",
foreignField: "item_id",
as: "item_history"
}
},
{
$project:
{
item_id: 1,
status: "$item_history.status",
imageUrl: 1
}
}])
Example Output
{
"_id" : NumberLong(26220),
"imageUrl" : "0bd2838e-9349-432a-a200-6e6b659e853eitemcompressed.jpg",
"status" : [ 1 ]
},
{
"_id" : NumberLong(26233),
"imageUrl" : "0bd2838e-9349-432a-a200-6e6b659e853eitemcompressed.jpg",
"status" : [ 1, 2 ]
}
One-to-one relationship
If there's only one corresponding history document for every item, you can use the following approach to get the exact format you requested:
Query
db.items.aggregate([
{
$lookup:
{
from: "user_item_history",
localField: "_id",
foreignField: "item_id",
as: "item_history"
}
},
{
$unwind: "$item_history"
},
{
$project:
{
item_id: 1,
status: "$item_history.status",
imageUrl: 1
}
}])
Example Output
{
"_id" : NumberLong(26220),
"imageUrl" : "0bd2838e-9349-432a-a200-6e6b659e853eitemcompressed.jpg",
"status" : 1
}
Please bear in mind that with every additional aggregation pipeline stage you add, the performance deteriorates. So you may prefer the one-to-many query even if you have a one-to-one relationship.
Applying filtering
In your edit, you added this:
we have to filter data for particular user and category of item. so user_id and category is in user_item_history collection
To filter your results, you should add a $match step to your query:
db.items.aggregate([
{
$lookup:
{
from: "user_item_history",
localField: "_id",
foreignField: "item_id",
as: "item_history"
}
},
{
$unwind: "$item_history"
},
{
$match:
{
"item_history.user_id": NumberLong(25416),
"item_history.catagory": "FOOD"
}
},
{
$project:
{
item_id: 1,
status: "$item_history.status",
imageUrl: 1
}
}])
Please note that "category" is misspelled as "catagory" in your example data, so I also had to misspell it in the query above.