mongodb `$lookup` or `join` with attributes inside array of objects - mongodb

I have this object that coming from mongodb
[
{
"_id": "5eaf2fc88fcee1a21ea0d94d",
"migration_customer_union_id": 517,
"__v": 0,
"account": 1,
"createdAt": "2020-05-03T20:55:36.335Z",
"customerUnion": "5eaf2fc7698de8321ccd841d",
"shaufel_customers": [
{
"percent": 50,
"_id": "5eaf2fc8698de8321ccd881f",
"customer": "5eaf2fb9698de8321ccd68c0"
},
{
"percent": 50,
"_id": "5eaf2fc9698de8321ccd8a9d",
"customer": "5eaf2fb9698de8321ccd68c0"
}
],
}
]
you can notice inside shaufel_customers array there is an attribute named customer which I want to use it to join with customers document, so that's what I am doing (wrote this code with help of stackoverflow :) )
const aggregate = await CustomerUnionCustomer.aggregate(
[
{
$match: {migration_customer_union_id: 517}
},
{
$lookup: {
from: 'customers',
localField: 'shaufel_customers.customer',
foreignField: '_id',
as: 'customers',
}
},
{
$project: {
shaufel_customer_union_id: 1,
customerUnion: '$customerUnions',
shaufel_customers: {
$map: {
input: "$customers",
as: "c",
in: {
$mergeObjects: [
"$$c",
{
$arrayElemAt: [{
$filter: {
input: "$shaufel_customers",
cond: {$eq: ["$$this.customer", "$$c._id"]}
}
}, 0]
},
]
}
},
}
}
},
{
"$project": { // this project just to get some specific values inside shaufel_customers
'_id': 0,
"shaufel_customers": {
"$map": {
"input": "$shaufel_customers",
"as": "customer",
"in": {
"customer_id": "$$customer.shaufel_customer_id",
"percent": "$$customer.percent"
}
}
}
}
}
]
)
when executing this code I am getting the following response
[
{
"shaufel_customers": [
{
"customer_id": "869",
"percent": 50
}
]
}
]
you can notice I got one object, although there was two objects inside the original array above, and that's because the customer attribute above has the same ObjectId value 5eaf2fb9698de8321ccd68c0 and that's what I want to ask. I want to get the same two objects even if the ids are the same, so the result I am expecting here is
[
{
"shaufel_customers": [
{
"customer_id": "869",
"percent": 50
},
{
"customer_id": "869",
"percent": 50
},
]
}
]
How should I do that :(

You need to revert your $map and iterate shaufel_customers instead of customer - this will return two results:
{
$project: {
shaufel_customer_union_id: 1,
customerUnion: '$customerUnions',
shaufel_customers: {
$map: {
input: "$shaufel_customers",
as: "sc",
in: {
$mergeObjects: [
"$$c",
{
$arrayElemAt: [{
$filter: {
input: "$customers",
cond: {$eq: ["$$this._id", "$$sc.customer"]}
}
}, 0]
},
]
}
},
}
}
},

Related

MongoDB - Lookup match with condition array of object with string

I have two collections "datasets" and "users".
I tried to lookup datasets.assignedTo = users.id that's working fine. Also, I want to match the field of datasets.firstBillable >= users.prices.beginDate date field are matched to get the current index price value. And also check users.prices.endDate is less than or equal to users.prices.beginDate.
For example:
cgPrices: 45
https://mongoplayground.net/p/YQps9EozlAL
Collections:
db={
users: [
{
id: 1,
name: "Aravinth",
prices: [
{
beginDate: "2022-08-24T07:29:01.639Z",
endDate: "2022-08-31T07:29:01.639Z",
price: 45
}
]
},
{
id: 2,
name: "Raja",
prices: [
{
beginDate: "2022-07-25T07:29:01.639Z",
endDate: "2022-07-30T07:29:01.639Z",
price: 55
}
]
}
],
datasets: [
{
color: "braun, rose gold",
firstBillable: "2022-08-24T07:29:01.639Z",
assignedTo: 1
},
{
color: "beige, silber",
firstBillable: "2022-07-25T07:29:01.639Z",
assignedTo: 2
}
]
}
My current implementation:
db.datasets.aggregate([
{
"$lookup": {
"from": "users",
"as": "details",
let: {
assigned_to: "$assignedTo",
first_billable: "$firstBillable"
},
pipeline: [
{
"$match": {
$expr: {
"$and": [
{
"$eq": [
"$id",
"$$assigned_to"
]
},
{
"$gte": [
"$first_billable",
"$details.prices.beginDate"
]
},
{
"$lte": [
"$first_billable",
"$details.prices.endDate"
]
}
]
}
}
}
]
}
},
{
"$addFields": {
"details": 0,
"cg": {
$first: {
"$first": "$details.prices.price"
}
}
}
}
])
Output i needed:
[
{
"_id": ObjectId("5a934e000102030405000000"),
"assignedTo": 1,
"cg": 45,
"color": "braun, rose gold",
"details": 0,
"firstBillable": "2022-08-24T07:29:01.639Z"
},
{
"_id": ObjectId("5a934e000102030405000001"),
"assignedTo": 2,
"cg": 55,
"color": "beige, silber",
"details": 0,
"firstBillable": "2022-07-25T07:29:01.639Z"
}
]
https://mongoplayground.net/p/YQps9EozlAL
Concerns:
You should compare the date as Date instead of string, hence you are required to convert the date strings to Date before comparing.
In users collection, prices is an array. You need to deconstruct the array to multiple documents first before compare the date fields in price.
The query should be:
db.datasets.aggregate([
{
"$lookup": {
"from": "users",
"as": "details",
let: {
assigned_to: "$assignedTo",
first_billable: {
$toDate: "$firstBillable"
}
},
pipeline: [
{
$match: {
$expr: {
$eq: [
"$id",
"$$assigned_to"
]
}
}
},
{
$unwind: "$prices"
},
{
"$match": {
$expr: {
"$and": [
{
"$gte": [
"$$first_billable",
{
$toDate: "$prices.beginDate"
}
]
},
{
"$lte": [
"$$first_billable",
{
$toDate: "$prices.endDate"
}
]
}
]
}
}
}
]
}
},
{
"$addFields": {
"details": 0,
"cg": {
$first: "$details.prices.price"
}
}
}
])
Demo # Mongo Playground

Mongo query for lookup array of keys which is itself an item in a nested array

My first collection is as below, I am searching the document with the email and match the particular jobid inside the jobs array. Then insert the document of second collection by matching _id with jobs.Process.profile_id.
{
"_id": {
"$oid": "6229d3cfdbfc81a8777e4821"
},
"jobs": [
{
"job_ID": {
"$oid": "62289ded8079821eb24760e0"
},
"Process": [
{
"profile_id": {
"$oid": "6285e571681188e83d434797"
}
},
{
"profile_id": {
"$oid": "6285e571681188e83d434799"
}
}
],
},
{
"job_ID": {
"$oid": "6228a252fb4554dd5c48202a"
},
"Process": [
{
"profile_id": {
"$oid": "62861067dc9771331e61df5b"
}
}
],
},
{
"job_ID": {
"$oid": "622af1c391b290d34701af9f"
},
"Process": [
""
],
}
],
"email": "********#gmail.com"
}
and my second collection is, I need to insert this document in my first collection by matching with jobs.Process.profile_id.
{
"_id": {
"$oid": "6285e571681188e83d434797"
},
"Name": "Lakshdwanan",
"Location":"California"
}
I have tried with query,
aggregate([
{ $match: { email: email } },
{
$lookup: {
from: 'user__profiles',
localField: 'jobs.Process.profile_id',
foreignField: '_id',
as: 'jobings',
},
},
{
$addFields: {
jobings: {
$map: {
input: {
$filter: {
input: '$jobs',
as: 'm',
cond: {
$eq: ['$$m.job_ID', objInstance],
},
},
},
as: 'm',
in: {
$mergeObjects: [
{
$arrayElemAt: [
{
$filter: {
input: '$jobings',
cond: {
$eq: ['$$this._id', '$$m.Process.profile_id'],
},
},
},
0,
],
},
'$$m',
],
},
},
},
},
},
{
$project: {
jobings: 1,
_id: 0,
},
},
]);
My output should only display second collection document based on the first collection document matching.
EDIT: If you want the data for a specific job only, it is better to $filter the jobs before the $lookup step. After the $lookup, just $unwind and format:
db.firstCol.aggregate([
{
$match: {email: email}
},
{
$project: {
jobs: {
$filter: {
input: "$jobs",
as: "item",
cond: {$eq: ["$$item.job_ID", objInstance]}
}
},
_id: 0
}
},
{
$lookup: {
from: "user__profiles",
localField: "jobs.Process.profile_id",
foreignField: "_id",
as: "jobings"
}
},
{
$project: {res: "$jobings", _id: 0}
},
{
$unwind: "$res"
},
{
$replaceRoot: {newRoot: "$res"}
}
])
Playground
The jobs.Process.profile_id is the user__profiles _id, so no need to merge anything...The results are documents from user__profiles collection "as is" but they can be formatted as wanted..._id key name can be renamed profile_id easily.

Lookup for a referenced document in an array of embedded documents

I got two collections.
One contains an array of objects. These objects own a field with an id to a document in another collection.
The goal is to "replace" the ref by the document. Sounds simple but I have no clue how to archive this.
E.G.
Collection "Product"
{ "_id": 1,
"alias": "ProductA"
},
{ "_id": 2,
"alias": "ProductC"
}
Collection "Order"
{ "_id": 5765,
"cart": [
{
"product": 1,
"qty": 7
}, {
"product": 2,
"qty": 6
}
]
}
What I need by a query is this:
{ "_id": 5765,
"cart": [
{
"product": {
"_id": 1,
"alias": "ProductA"
},
"qty": 7
}, {
"product": {
"_id": 2,
"alias": "ProductC"
},
"qty": 6
}
]
}
I tried a simple lookup, but the array will only contains the products. What do I need to change?
{
$lookup: {
from: "products",
let: {
tmp: "$cart.product"
},
pipeline: [
{
$match: {
$expr: {
$in: ["$_id", "$$tmp"]
}
}
}
],
as: "cart.product"
}
}
Thanks for your help.
I added a new $addFields stage to transform the output from the $lookup stage - it gets the desired output:
db.order.aggregate([
{
$lookup: {
from: "product",
let: {
tmp: "$cart.product"
},
pipeline: [
{
$match: {
$expr: {
$in: ["$_id", "$$tmp"]
}
}
}
],
as: "products"
}
},
{
$addFields: {
cart: {
$map: {
input: "$cart", as: "ct",
in: {
product: {
$arrayElemAt: [
{ $filter: {
input: "$products", as: "pr",
cond: {
$eq: [ "$$ct.product", "$$pr._id" ]
}
}
}, 0 ]
},
qty: "$$ct.qty"
}
}
}
}
}
] ).pretty()

Mongo aggregation. Return array element that matches multiple conditions as a new field. (reproduction included)

Current query: https://mongoplayground.net/p/mCD3vLoGG1q
Context: Users cast upvotes or downvotes on suggestions. Suggestions and votes are in their own collection. I am using $lookup to get all votes for a suggestion, it becomes an array during the aggregation.
I want the total number of votes per suggestion, but also vote data on 2 specific users.
The logged in user
The broadcaster user.
This "vote data" can be a simple boolean (did the user upvote), or the array element itself (preferred). I just need to know how they voted on a suggestion.
Current result
[
{
"_id": ObjectId("5a934e000102030405000000"),
// WRONG. The broadcaster downvoted.
"broadcasterUpvoted": true,
"hasUpvoted": true,
"id": "sid",
"votesLength": 2
}
]
Desired result
[
{
"_id": ObjectId("5a934e000102030405000000"),
// CORRECT!!!
"broadcasterUpvoted": false,
"hasUpvoted": true,
"id": "sid",
"votesLength": 2
}
]
Getting the array element could be useful, so this result is good too.
[
{
"_id": ObjectId("5a934e000102030405000000"),
"broadcasterVote": {
"suggestionId": "sid",
"voteType": "downVote",
"user": {
"id": "broadcasterUserId"
}
},,
"loggedInUserVote": {
"suggestionId": "sid",
"voteType": "upVote",
"user": {
"id": "loggedInUser"
}
},
"id": "sid",
"votesLength": 2
}
]
EDIT: Another question is, how do I make votesLength = (totalUpVotes - totalDownVotes) instead of just sizeOfVotesArray
Try this one:
db.suggestions.aggregate([
{
$match: {
id: "sid"
}
},
{
$lookup: {
from: "votes",
localField: "id",
foreignField: "suggestionId",
as: "votes"
}
},
{
$addFields: {
hasUpvoted: {
$filter: {
input: "$votes",
cond: { $eq: [ "apiCallUserId", "$$this.user.id"] }
}
},
broadcasterUpvoted: {
$filter: {
input: "$votes",
cond: { $eq: [ "broadcasterUserId", "$$this.user.id"]}
}
}
}
},
{
$project: {
_id: 1,
id: 1,
votesLength: {
$reduce: {
input: "$votes",
initialValue: 0,
in: {
$add: [
"$$value",
{
$switch: {
branches: [
{
case: { $eq: ["$$this.voteType","upVote"]},
then: 1
},
{
case: { $eq: ["$$this.voteType", "downVote"]},
then: -1
}
],
default: 0
}
}
]
}
}
},
broadcasterUpvoted: {
$arrayElemAt: [
"$broadcasterUpvoted",
0
]
},
hasUpvoted: {
$arrayElemAt: [
"$hasUpvoted",
0
]
}
}
}
])
MongoPlayground

MongoDB Aggregation - Lookup pipeline not returning any documents

I'm having hard time getting $lookup with a pipeline to work in MongoDB Compass.
I have the following collections:
Toys
Data
[
{
"_id": {
"$oid": "5d233c3bb173a546386c59bb"
},
"type": "multiple",
"tags": [
""
],
"searchFields": [
"Jungle Stampers - Two",
""
],
"items": [
{
"$oid": "5d233c3cb173a546386c59bd"
},
{
"$oid": "5d233c3cb173a546386c59be"
},
{
"$oid": "5d233c3cb173a546386c59bf"
},
{
"$oid": "5d233c3cb173a546386c59c0"
},
{
"$oid": "5d233c3cb173a546386c59c1"
},
{
"$oid": "5d233c3cb173a546386c59c2"
},
{
"$oid": "5d233c3cb173a546386c59c3"
},
{
"$oid": "5d233c3cb173a546386c59c4"
}
],
"name": "Jungle Stampers - Two",
"description": "",
"status": "active",
"category": {
"$oid": "5cfe727cac920000086b880e"
},
"subCategory": "Stamp Sets",
"make": "",
"defaultCharge": null,
"defaultOverdue": null,
"sizeCategory": {
"$oid": "5d0cfde57561e107c88fbde3"
},
"ageFrom": {
"$numberInt": "24"
},
"ageTo": {
"$numberInt": "120"
},
"images": [
{
"_id": {
"$oid": "5d233c3bb173a546386c59bc"
},
"id": {
"$oid": "5d233c39b173a546386c59ba"
},
"url": "/toyimages/5d233c39b173a546386c59ba.jpg",
"thumbUrl": "/toyimages/thumbs/tn_5d233c39b173a546386c59ba.jpg"
}
],
"__v": {
"$numberInt": "2"
}
}
]
Loans
Data
[
{
"_id": {
"$oid": "5e1f1661b712215978c746d9"
},
"tags": [],
"member": {
"$oid": "5e17495e4f81ab3f900dbb63"
},
"source": "admin portal - potter1#gmail.com",
"items": [
{
"id": {
"$oid": "5e1f160eb712215978c746d5"
},
"status": "new",
"_id": {
"$oid": "5e1f1661b712215978c746db"
},
"toy": {
"$oid": "5d233c3bb173a546386c59bb"
},
"cost": {
"$numberInt": "0"
}
},
{
"id": {
"$oid": "5e1f160eb712215978c746d5"
},
"status": "new",
"_id": {
"$oid": "5e1f1661b712215978c746da"
},
"toy": {
"$oid": "5d233b1ab173a546386c59b5"
},
"cost": {
"$numberInt": "0"
}
}
],
"dateEntered": {
"$date": {
"$numberLong": "1579095632870"
}
},
"dateDue": {
"$date": {
"$numberLong": "1579651200000"
}
},
"__v": {
"$numberInt": "0"
}
}
]
I am trying to return a list of toys and their associated loans that have a status of 'new' or 'out'.
I can use the following $lookup aggregate to fetch all loans:
{
from: 'loans',
localField: '_id',
foreignField: 'items.toy',
as: 'loansSimple'
}
However I am trying to use a pipeline to load loans that have the two statuses I am interested in, but it always only returns zero documents:
{
from: 'loans',
let: {
'toyid': '$_id'
},
pipeline: [
{
$match: {
$expr: {
$and: [
{$eq: ['$items.toy', '$$toyid']},
{$eq: ['$items.status', 'new']} // changed from $in to $eq for simplicity
]
}
}
}
],
as: 'loans'
}
This always seems to return 0 documents, however I arrange it:
Have I made a mistake somewhere?
I'm using MongoDB Atlas, v4.2.2, MongoDB Compass v 1.20.4
You are trying to search $$toyid inside inner array, but Operator Expression $eq cannot resolve it.
Best solution: $let (returns filtered loans by criteria) + $filter (applies filter for inner array) operator helps us to get desired result.
db.toys.aggregate([
{
$lookup: {
from: "loans",
let: {
"toyid": "$_id",
"toystatus": "new"
},
pipeline: [
{
$match: {
$expr: {
$gt: [
{
$size: {
$let: {
vars: {
item: {
$filter: {
input: "$items",
as: "tmp",
cond: {
$and: [
{
$eq: [
"$$tmp.toy",
"$$toyid"
]
},
{
$eq: [
"$$tmp.status",
"$$toystatus"
]
}
]
}
}
}
},
in: "$$item"
}
}
},
0
]
}
}
}
],
as: "loans"
}
}
])
MongoPlayground
Alternative solution 1. Use $unwind to flatten items attribute. (We create extra field named tmp which stores items value, flatten it with $unwind operator, match as you were doing and then exclude from result)
db.toys.aggregate([
{
$lookup: {
from: "loans",
let: {
"toyid": "$_id"
},
pipeline: [
{
$addFields: {
tmp: "$items"
}
},
{
$unwind: "$tmp"
},
{
$match: {
$expr: {
$and: [
{
$eq: [
"$tmp.toy",
"$$toyid"
]
},
{
$eq: [
"$tmp.status",
"new"
]
}
]
}
}
},
{
$project: {
tmp: 0
}
}
],
as: "loans"
}
}
])
MongoPlayground
Alternative solution 2. We use $reduce to create toy's array and with $in operator we check if toyid exists inside this array.
db.toys.aggregate([
{
$lookup: {
from: "loans",
let: {
"toyid": "$_id"
},
pipeline: [
{
$addFields: {
toys: {
$reduce: {
input: "$items",
initialValue: [],
in: {
$concatArrays: [
"$$value",
[
"$$this.toy"
]
]
}
}
}
}
},
{
$match: {
$expr: {
$in: [
"$$toyid",
"$toys"
]
}
}
},
{
$project: {
toys: 0
}
}
],
as: "loans"
}
}
])
$expr receives aggregation expressions, At that point $$items.toy is parsed for each element in an array as you would expect (however if it would it will still give you "bad" results as you'll get loans that have the required toy id and any other item with status new in their items array).
So you have two options to work around this:
If you don't care about the other items in the lookup'd document you can add an $unwind stage at the start of the lookup pipeline like so:
{
from: 'loans',
let: {
'toyid': '$_id'
},
pipeline: [
{
$unwind: "$items"
},
{
$match: {
$expr: {
$and: [
{$eq: ['$items.toy', '$$toyid']},
{$eq: ['$items.status', 'new']} // changed from $in to $eq for simplicity
]
}
}
}
],
as: 'loans'
}
If you do care about them just iterate the array in one of the possible ways to get a 'correct' match, here is an example using $filter
{
from: 'loads',
let: {
'toyid': '$_id'
},
pipeline: [
{
$addFields: {
temp: {
$filter: {
input: "$items",
as: "item",
cond: {
$and: [
{$eq: ["$$item.toy", "$$toyid"]},
{$eq: ["$$item.status", "new"]}
]
}
}
}
}
}, {$match: {"temp.0": {exists: true}}}
],
as: 'loans'
}