$lookup with deeply nested object - javascript

I am new to MongoDB and currently working on a recipe App for school which suggests diet plans. Therefore I need to join the "meal" ObjectId in the diet plan of the user (collection "Users") with the ObjectIds in the collections "Meals".
Afterwards I need to join an "ingredient" ObjectID in the "Meals" collection with the ID of the "ingredient" in the "Ingredients" collection. The problem is, that the "ingredient" ObjectID in collection "Meals" is situated in an Object with another integer variable "amount". This object is nested in an array called "ingredients" with many objects such as the one just described.
Below is my Structure:
Users
{
"_id": ObjectId("5b28cab902f28e18b863bd36"),
"username: "testUser1",
"password": "$2a$08$KjddpaSQPjp6aF/gseOhVeddYdqWJCJ4DpFwxfNgsk81G.0TOtN5i",
"dietPlans": Object
{
"dietPlanCurrent":Object
{
"monday":Object
{
"breakfast":Object
{
"meal": ObjectId("5b2b9a8bbda339352cc39ec4")
},
…
},
…
},
…
},
}
Meals
{
"_id" : ObjectId("5b2b9a8bbda339352cc39ec4"),
"name": "Gulasch-breakfast",
"cuisine": "International",
"ingredients":[
{
"ingredient": ObjectId("5b1ec0f939b55efcd4e28a2d"),
"amount": 20
},
{
"ingredient": ObjectId("5b1ec42474fc1f58d84264d4"),
"amount": 20
},
{
"ingredient": ObjectId("5b1ec42474fc1f58d84264d5"),
"amount": 20
},
…
],
"comments": [
…
]
}
Ingredients
{
{
"_id": ObjectId("5b1ec0f939b55efcd4e28a2d"),
"name": "Walnut",
"calories": 654
…
}
{
"_id": ObjectId("5b1ec0f939b55efcd4e28a3d"),
"name": "Apple",
"calories": 123
…
}
…
}
What I am trying to get is:
{
"_id": ObjectId("5b28cab902f28e18b863bd36"),
"username: "testUser1",
"password": "$2a$08$KjddpaSQPjp6aF/gseOhVeddYdqWJCJ4DpFwxfNgsk81G.0TOtN5i",
"dietPlans": Object
{
"dietPlanCurrent":Object
{
"Monday":Object
{
"breakfast":Object
{
"meal": ObjectId("5b2b9a8bbda339352cc39ec4")
"matchedIngredients": [
{
"_id": ObjectId("5b1ec0f939b55efcd4e28a2d"),
"name": "Walnut",
"calories": 654
…
}
…
]
},
…
},
…
},
…
},
}
My approach which is not working (only returning empty matchedIngredients Array)
{
$match: {
'_id': mongoose.Types.ObjectId(req.params.userId)
}
},
{
$lookup: {
from: 'meals',
localField: 'dietPlans.dietPlanCurrent.monday.breakfast.meal',
foreignField: '_id',
as: "dietPlans.dietPlanCurrent.monday.breakfast.mealObject"
}
},
{
$unwind: {
path: "$dietPlans.dietPlanCurrent.monday.breakfast.mealObject",
preserveNullAndEmptyArrays: true
}
},
{
$unwind: {
path: "$dietPlans.dietPlanCurrent.monday.breakfast.mealObject.ingredients",
preserveNullAndEmptyArrays: true
}
},
{
$lookup: {
from: 'ingredients',
localField: 'dietPlans.dietPlanCurrent.monday.breakfast.mealObject.ingredients.ingredient',
foreignField: '_id',
as: "dietPlans.dietPlanCurrent.monday.breakfast.matchedIngredients"
}
}
Help is very much appreciated. I already checked out this approach, but it somehow didn't work:
Approach that didn't work for me
Thank you very much!

What you are trying to do is not possible with mongodb version 3.4 but if you upgrade to 3.6 then you can try below aggregation
db.collection.aggregate([
{ "$match": { "_id": mongoose.Types.ObjectId(req.params.userId) } },
{ "$lookup": {
"from": Meals.collection.name,
"let": { "meal_id": "$dietPlans.dietPlanCurrent.monday.breakfast.meal" },
"pipeline": [
{ "$match": { "$expr": { "$eq": [ "$_id", "$$meal_id" ] } } },
{ "$unwind": "$ingredients" },
{ "$lookup": {
"from": Ingredients.collection.name,
"let": { "ingredient_id": "$ingredients.ingredient" },
"pipeline": [
{ "$match": { "$expr": { "$eq": [ "$_id", "$$ingredient_id" ] } } }
],
"as": "matchedIngredients"
}},
{ "$unwind": "$ingredients.matchedIngredients" },
{ "$group": {
"_id": "$_id",
"name": { "$first":"$name" },
"cuisine": { "$first":"$cuisine" },
"ingredients": { "$push":"$ingredients" }
}}
],
"as": "dietPlans.dietPlanCurrent.monday.breakfast.mealObject"
}},
{ "$unwind": "$dietPlans.dietPlanCurrent.monday.breakfast.mealObject" }
])

Related

Find user is registered to a Event in MongoDb (aggregation)

I tried to find users who are registered for that event.
So I join multiple collections shown below -
Events.aggregate([
{ $match: { category: "group_event" } },
// collection where events are scheduled
{
$lookup: {
from: "group_events",
let: { eventId: "$eventID" },
pipeline: [
{
$match: {
$expr: {
$and: [
{ $eq: ["$_id", "$$eventId"] },
{ $gt: ["$time", new Date()] },
],
},
},
},
// register user collection
{
$lookup: {
from: "register_events",
let: { eventId: "$_id" },
pipeline: [
{ $match: { $expr: { $eq: ["$eventId", "$$eventId"] } } },
],
as: "registerUsers",
},
},
],
as: "events",
},
},
{ $unwind: "$events" },
])
and the output is now comingout -
[
{
"_id": "614d6dfd82cb36be231083c9",
"trainerId": "61488dc36b7ccedbc884d20a",
"category": "group_event",
"eventID": "614d6dfc82cb36be231083c7",
"createdAt": "2021-09-24T06:19:41.268Z",
"updatedAt": "2021-09-24T06:19:41.268Z",
"__v": 0,
"events": {
"_id": "614d6dfc82cb36be231083c7",
"groupName": "group name 4",
"category": "sdfsdf",
"time": "2021-09-27T07:44:58.762Z",
"description": "description",
"day": "sunday",
"platform": "zoom",
"notes": "22",
"skills_to_learn": [
"demo"
],
"status": "pending",
"trainerId": "61488dc36b7ccedbc884d20a",
"meetingLink": "https://us05web.zoom.us/j/81660534858?pwd=cGZaODVjdWJUQWNtN243MlNiVUN0UT09",
"type": "group_event",
**isUserRegisted : true / false,**
"createdAt": "2021-09-24T06:19:41.000Z",
"updatedAt": "2021-09-24T06:19:41.000Z",
"__v": 0,
"registerUsers": [
{
"_id": "614ed6b4b8a545acb8517e85",
"userId": "614d59371d11becb8e23f536",
"eventId": "614d6dfc82cb36be231083c7",
"question": "",
"createdAt": "2021-09-25T07:58:44.939Z",
"updatedAt": "2021-09-25T07:58:44.939Z",
"__v": 0
}
]
}
}
]
which is ok for me bu just wanted to add a key: value, heighlited on obove section
isUserRegisted : true / false
i tried with $addFields but can't came up with any solution. Basically I need to retrieve arrays from registerUsers - collection and on the same time match the userId
I was able to figure out this issue.
simply I need to use $project and $filter to get the data if available and at last use $cond to return true or false
{
$project: {
root: "$$ROOT",
userFound: {
$filter: {
input: "$registerUsers",
as: "ac",
cond: {
$eq: ["$$ac.userId", mongoose.Types.ObjectId(userId)],
},
},
},
},
},
{
$project: {
_id: 0,
document: "$$ROOT",
userFound: {
$cond: {
if: { $isArray: "$userFound" },
then: {
$cond: {
if: {
$gt: [{ $size: "$userFound" }, 0],
},
then: true,
else: false,
},
},
else: false,
},
},
},
},
// merging nested object with parents
{
$replaceRoot: {
newRoot: {
$mergeObjects: [
"$document.root",
{ isUserRegistered: "$userFound" },
],
},
},
},

MongoDB - Structure an array without using key field in Aggregration

I'm having an issue with making count for items returned from an array without assuming or using those fields in my aggregration.
Data structure looks like this:
[
{
"_id": "1",
"title": "Vanella Icream",
"contain": "sugar",
"details": [
{
"flavour": "Vanella"
},
{
"weight": "10KG"
},
{
"sugar": "15KG"
}
]
},
{
"_id": "2",
"title": "Pretzels",
"contain": "salt",
"details": [
{
"flavour": "Wheat"
},
{
"weight": "10KG"
},
{
"sugar": "15KG"
}
]
},
{
"_id": "3",
"title": "Rasmalai Icream",
"contain": "sugar",
"details": [
{
"flavour": "Vanella"
},
{
"weight": "15KG"
},
{
"sugar": "12KG"
}
]
},
{
"_id": "4",
"title": "Vanella Icream",
"contain": "sugar",
"details": [
{
"flavour": "Vanella"
},
{
"weight": "15KG"
},
{
"sugar": "12KG"
}
]
}
]
Output I want:
[
{
"details": {
"flavour": {
"Vanella": 3, //Number of times Vanella present in each document.
"Wheat": 1,
},
"weight": {
"10KG": 2,
"15KG": 2
},
"sugar": {
"12KG": 2,
"15KG": 2
}
}
}
]
Query:
db.collection.aggregate([
{
"$unwind": {
"path": "$details"
}
},
{
"$replaceRoot": {
"newRoot": {
"$mergeObjects": [
"$details",
"$$ROOT"
]
}
}
},
{
"$facet": {
"flavour": [
{
"$group": {
"_id": "$flavour",
"sum": {
"$sum": 1
}
}
},
{
"$addFields": {
"flavour": "$_id"
}
},
{
"$project": {
"_id": 0
}
}
],
"weight": [
{
"$group": {
"_id": "$weight",
"sum": {
"$sum": 1
}
}
},
{
"$addFields": {
"weight": "$_id"
}
},
{
"$project": {
"_id": 0
}
}
]
}
},
{
"$addFields": {
"flavour": {
"$reduce": {
"input": {
"$filter": {
"input": {
"$map": {
"input": "$flavour",
"as": "w",
"in": {
"$cond": [
{
"$ne": [
"$$w.flavour",
null
]
},
{
"$let": {
"vars": {
"o": [
[
"$$w.flavour",
"$$w.sum"
]
]
},
"in": {
"$arrayToObject": "$$o"
}
}
},
null
]
}
}
},
"as": "f",
"cond": {
"$ne": [
"$$f",
null
]
}
}
},
"initialValue": {},
"in": {
"$let": {
"vars": {
"d": "$$value",
"p": "$$this"
},
"in": {
"$mergeObjects": [
"$$d",
"$$p"
]
}
}
}
}
},
"weight": {
"$reduce": {
"input": {
"$filter": {
"input": {
"$map": {
"input": "$weight",
"as": "w",
"in": {
"$cond": [
{
"$ne": [
"$$w.weight",
null
]
},
{
"$let": {
"vars": {
"o": [
[
"$$w.weight",
"$$w.sum"
]
]
},
"in": {
"$arrayToObject": "$$o"
}
}
},
null
]
}
}
},
"as": "f",
"cond": {
"$ne": [
"$$f",
null
]
}
}
},
"initialValue": {},
"in": {
"$let": {
"vars": {
"d": "$$value",
"p": "$$this"
},
"in": {
"$mergeObjects": [
"$$d",
"$$p"
]
}
}
}
}
}
}
},
{
"$project": {
"details": "$$ROOT"
}
}
])
Here I'm trying to get the flavour and weight with their count, with manually adding those fields in $filter stage. I want to do it without assuming those keys. So, even if there is 20 items present in array details it will map those items and shows me output with their counts respectively.
I hope you guys understand.
Playground:https://mongoplayground.net/p/j1mzgWvcmvd
You need to change the schema, the thing you want to do is easy, and both those queries are so complicated and slow, even the second that is much smaller has 2 $unwind and 3 $group with 3 $arrayToObject and 8 stages total because of the schema and the schema of the answer.
Don't store data in the keys of the documents, people that are new to MongoDB do those, i was doing it also, but it makes all things harder.(i can't say like never do it but you dont need it here)
Your schema should be something like
{
"_id": "2",
"title": "Pretzels",
"contain": "salt",
"details": [
{
"type" : "flavour",
"value" : "Wheat"
},
{
"type" : "weight",
"value" : "10KG"
},
{
"type" : "sugar",
"value" : "15KG"
}
]
}
See this example
Converts your schema, to the new schema and produce the results you
want but without data in keys (the first part you wouldnt need it you would need only the bellow query if you had that schema from start)
Query with the new Schema (no data in keys)
[{"$unwind": { "path": "$details"}},
{"$replaceRoot": {"newRoot": "$details"}},
{
"$group": {
"_id": {
"type": "$type",
"value": "$value"
},
"sum": {"$sum": 1}
}
},
{
"$replaceRoot": {
"newRoot": {"$mergeObjects": ["$_id","$$ROOT"]}
}
},
{"$project": {"_id": 0}},
{
"$group": {
"_id": "$type",
"values": {
"$push": {
"value": "$value",
"sum": "$sum"
}
}
}
},
{"$addFields": {"type": "$_id"}},
{"$project": {"_id": 0}}
]
MongoDB operators are not made to support for data in keys or dynamic keys(uknown keys) (to do it you do complicated things like the above)
If you want to change your schema, either do it with update in the database,
Or take the documents to the application and do it with javascript, and re-insert.
Even if you solve this question in the next one, you will have again problems.
I'm the guy from Mongodb Forum:
Try this out https://mongoplayground.net/p/tfyfpIkHilQ

join object from another documnt with localfield key

i have a competition doc with field teams array of object with _id of team and a score doc with teamId field
competitions.teams = [{_id: 100,..}, {..}]
score.teamId = 100
when aggregatig score i want to group it to the competition teams but imm getting all team inside the group innstead of matching id
sample document https://mongoplayground.net/p/yJ34IBnnuf5
db.scores.aggregate([
{
"$match": {
"type": "league"
}
},
{
"$lookup": {
"from": "competitions",
"localField": "competitionId",
"foreignField": "_id",
"as": "comp"
}
},
{
"$unwind": {
"path": "$comp",
"preserveNullAndEmptyArrays": true
}
},
{
"$project": {
"comp.teams": 1,
"teamId": 1
}
},
{
"$group": {
"_id": "$teamId",
"results": {
"$push": "$comp.teams"
}
}
}
])
returns all team in group instead of matched teamid
{
"_id" : 100
"results" : [
{
"_id": 100,
"name": "team 1"
},
{
"_id": 101,
"name": "team 2"
}
]
}
{
"_id" 101
"results" : [
{
"_id": 100,
"name": "team 1"
},
{
"_id": 101,
"name": "team 2"
}
]
}
this is the result im trying to accomplish please guide me
{
"_id" : 100
"results" : [
{
"_id": 100,
"name": "team 1"
}
]
}
{
"_id" 101
"results" : [
{
"_id": 101,
"name": "team 2"
}
]
}
what should i do i've read the docs this seems to be the way?
Demo - https://mongoplayground.net/p/ETeroLftcZZ
You have to add $unwind: { "path": "$comp.teams" }
and after that group by { $group: { "_id": "$comp.teams._id" ... }
db.scores.aggregate([
{ $match: { "type": "league" } },
{ $lookup: { "from": "competitions", "localField": "competitionId", "foreignField": "_id", "as": "comp" } },
{ $unwind: { "path": "$comp", "preserveNullAndEmptyArrays": true } },
{ $unwind: { "path": "$comp.teams", "preserveNullAndEmptyArrays": true }},
{ $group: { "_id": "$comp.teams._id", "results": { $push: "$comp.teams" } } }
])
Demo with more data - https://mongoplayground.net/p/b41Ch5ge2Wp

How to add a condition inside $lookup in MongoDb .aggregate()?

persons collection:
{
"_id": ObjectId("5f3258cfbaaccedaa5dd2c96"),
"gender": "male",
"name": {
"title": "mr",
"first": "victor",
"last": "pedersen",//... more properties
}
persondetails collection:
{
"_id": ObjectId("5f3a91e68b1c26e68f9ed3ad"),
"country": "India",
"personid": ObjectId("5f3258cfbaaccedaa5dd2c96")
}
Get documents from persons along with associated persondetails where associated country in persondetails is "India
So if out of 10 persons only 3 are from India. I should get 3 person documents in resultset along with their associated persondetails
Query:
[
{
"$match": {
"$or": [
{
"$expr": {
"$eq": [
"$gender",
"male"
]
}
}
]
}
},
{
"$facet": {
"totalCount": [
{
"$count": "value"
}
],
"data": [
{
"$project": {
"_id": "$_id",
"fname": "$name.first",
"lname": "$name.last",
"dobage": "$dob.age",
"registeredAge": "$registered.age"
}
},
{
"$sort": {
"name.first": 1
}
} ,
{
"$lookup": {
"from": "persondetails",
"localField": "_id",
"foreignField": "personid", // how to add where clause in this lookup?
"as": "persondetail"
}
}
]
}
}
]
EDIT:
https://mongoplayground.net/p/3vBs6Frt-aK
Expected Result:
[
{
"data": [
{
"_id": ObjectId("5f3258cfbaaccedaa5dd2c96"),
"fname": "victor",
"lname": "pedersen",
"persondetail": [
{
"_id": ObjectId("5f3a91e68b1c26e68f9ed3ad"),
"country": "India",
"personid": ObjectId("5f3258cfbaaccedaa5dd2c96")
}
]
}],
"totalCount": [
{
"value": 1
}
]
}
]
There is a second $lookup syntax which allows you to specify custom filtering condition:
{
"$lookup": {
"from": "persondetails",
"let": { person_id: "$_id" },
"pipeline": [
{
$match: {
$expr: {
$and: [
{ $eq: [ "$$person_id", "$personid" ] },
{ $eq: [ "$country", "India" ] },
]
}
}
}
],
"as": "persondetail"
}
}
EDIT:
You also need to add $match to filter out people with empty persondetail and if you want this to be included in your count then you need to run $facet as the last operation:
[
{
"$match": {
"$or": [ {"$expr": { "$eq": [ "$gender", "male" ] } } ]
}
},
{
"$project": {
"_id": "$_id",
"fname": "$name.first",
"lname": "$name.last",
"dobage": "$dob.age",
"registeredAge": "$registered.age"
}
},
{
"$lookup": {
"from": "persondetails",
"let": { person_id: "$_id" },
"pipeline": [
{
$match: {
$expr: {
$and: [
{ $eq: [ "$$person_id", "$personid" ] },
{ $eq: [ "$country", "India" ] },
]
}
}
}
],
"as": "persondetail"
}
},
{
$match: {
persondetail: { $ne: [] }
}
},
{
$facet: {
totalCount: [ { $count: "value" } ],
data: [ { $sort: { "name.first": 1 } }, ]
}
}
]
Mongo Playground

How to change field from subdocument a parent field in Mongoose

I am trying to export Mongo data to XLSX which requires all the data to be in the parent map but currently I have data in this format:
[
{
"_id": "eatete",
"competition": {
"_id": "eatete"
"name": "Some competition name"
},
"members": [
{
"_id": "eatete",
"name": "Saad"
},
{
"_id": "eatete",
"name": "Saad2"
}
],
"leader": {
"name": "Saad",
"institute": {
"_id": "eatete",
"name": "Some institute name"
}
},
}
]
Ideally, the data should be:
[
{
"_id": "eatete",
"competition": "Some competition name"
"member0name": "Saad",
"member1name": "Saad2",
"leadername": "Saad",
"institute": "Some institute name"
}
]
So basically what I want is to refer the data of fields of subdocuments as if those were part of parent document, like competitions = competitions.name.
Can you please help me how can I do so using Mongoose.
Thanks
With some more aggregation trick
db.collection.aggregate([
{ "$unwind": { "path": "$members", "includeArrayIndex": "i" }},
{ "$group": {
"_id": "$_id",
"competition": { "$first": "$competition.name" },
"leadername": { "$first": "$leader.name" },
"institute": { "$first": "$leader.institute.name" },
"data": {
"$push": {
"k": { "$concat": ["members", { "$toLower": "$i" }, "name"] },
"v": "$members.name"
}
}
}},
{ "$replaceRoot": {
"newRoot": {
"$mergeObjects": ["$$ROOT", { "$arrayToObject": "$data" }]
}
}},
{ "$project": { "data": 0 }}
])
You can try below aggregation on your Model:
let resultt = await Model.aggregate([
{
$project: {
_id: 1,
competition: "$competition.name",
leadername: "$leader.name",
institute: "$leader.institute.name",
members: {
$map: {
input: { $range: [ 0, { $size: "$members" } ] },
in: {
k: { $concat: [ "member", { $toString: "$$this" }, "name" ] },
v: {
$let: {
vars: { current: { $arrayElemAt: [ "$members", "$$this" ] } },
in: "$$current.name"
}
}
}
}
}
}
},
{
$replaceRoot: {
newRoot: {
$mergeObjects: [ "$$ROOT", { $arrayToObject: "$members" } ]
}
}
},
{
$project: {
members: 0
}
}
])
Since you need to dynamically evaluate your keys based on indexes you can use $map with $range to map a list of indexes into keys of a new object. Then you can use $arrayToObject to get an object from those keys and $mergeObjects with $replaceRoot to flatten this object structure.

Categories

Resources