Join two collections with id stored in array of objects in mongodb - mongodb

I have two collections with name School & Students
School Collection
{
_id: ObjectId("60008e81d186a82fdc4ff2b7"),
name: String,
branch: String,
class: [{
"active" : true,
"_id" : ObjectId("6001e6871d985e477b61b43f"),
"name" : "I",
"order" : 1
},
{
"active" : true,
"_id" : ObjectId("6001e68f1d985e477b61b444"),
"name" : "II",
"order" : 2
}]
}
Student Collection
{
_id: ObjectId("6002def815eccd53a596f830"),
schoolId: ObjectId("60008e81d186a82fdc4ff2b7"),
sessionId: ObjectId("60008e81d186a82fdc4ff2b9"),
class: ObjectId("6001e6871d985e477b61b43f"),
}
I want to get the data of Student Collection in single query.
I have class id stored in Student Collection and data against that id is stored in School Collection under class key, which is array of objects.
Can you please help me in getting the class object in student collection with this id?
Output i want:
data: {
_id: ObjectId("6002def815eccd53a596f830"),
schoolId: ObjectId("60008e81d186a82fdc4ff2b7"),
sessionId: ObjectId("60008e81d186a82fdc4ff2b9"),
class: ObjectId("6001e6871d985e477b61b43f"),
classData: [{
"active" : true,
"_id" : ObjectId("6001e6871d985e477b61b43f"),
"name" : "I",
"order" : 1
}]
}
So I tried this but it didn't work:
const students = await this.studentModel.aggregate([
{
$lookup: {
from: 'School',
let: { classId: '$class' },
pipeline: [
{
$match: {
$expr: { $eq: ['$$classId', '$class._id'] },
},
},
],
as: 'classData',
},
},
]);

$lookup, pass schoolId and class in let,
$match school id condition
$filter to iterate loop of class and filter specific class object
$arrayElemAt will get object from returned result from $filter
$replaceRoot to replace object to root
const students = await this.studentModel.aggregate([
{
$lookup: {
from: "School",
let: {
schoolId: "$schoolId",
classId: "$class"
},
pipeline: [
{ $match: { $expr: { $eq: ["$$schoolId", "$_id"] } } },
{
$replaceRoot: {
newRoot: {
$arrayElemAt: [
{
$filter: {
input: "$class",
cond: { $eq: ["$$this._id", "$$classId"] }
}
},
0
]
}
}
}
],
as: "classData"
}
}
])
Playground

Related

MongoDB multiple $lookup and $group output

I'm quite a newbie with MongoDB and I'm trying to retrieve a kind-of leaderboard based on two related collections and a third one, referencing one of the two, based on its different property.
Schema can be found here
Consider a schema like the following one:
tree: { _id, company_id: string, company_name }
link: { _id, company_id: string, url: string }
analytics: { _id, tree_id: string, link_id: string, views: number, clicks: number, date: stringĀ }
A analytics document can have tree_id, views or link_id, clicks at once.
What I'm trying to achieve right now is a kind-of a "leaderboard" of the total clicks + views, starting from analytics collection, joining it with both tree and link, and finally retrieving the sum of clicks and views.
I have already managed to retrieve the sum of them for a specific company_id, with the following code
db.analytics.aggregate([{
$lookup: {
from: "trees",
as: "trees",
localField: "tree_id",
foreignField: "_id"
}
}, {
$lookup: {
from: "links",
as: "links",
localField: "link_id",
foreignField: "_id"
}
}, {
$match: {
$or: [
{"trees.company_id": "1"},
{"links.company_id": "1"}
]
}
}, {
$group: {
_id: null,
views_count: {
$sum: "$views"
},
clicks_count: {
$sum: "$clicks"
}
}
}])
But I can't find a way to get a list of results like
{ company_id: 1, company_name: "foo", clicks: 100, views: 200 },
{ company_id: 2, company_name: "bar", clicks: 200, views: 200 }
and so on.
What I've tried so far is grouping by different _id, which is not working as I would expect
db.analytics.aggregate([{
$lookup: {
from: "trees",
as: "trees",
localField: "tree_id",
foreignField: "_id"
}
}, {
$lookup: {
from: "links",
as: "links",
localField: "link_id",
foreignField: "_id"
}
}, {
$group: {
_id: "$trees.company_id",
views_count: {
$sum: "$views"
},
clicks_count: {
$sum: "$clicks"
}
}
}])
Which does not assign clicks_count to a specific entry, but outputs something like
{ "_id" : [ "1" ], "views_count" : 6, "clicks_count" : 0 }
{ "_id" : [ ], "views_count" : 0, "clicks_count" : 48 }
{ "_id" : [ "2" ], "views_count" : 10, "clicks_count" : 0 }
I'm not even sure that this schema could be the best solution, so I will also appreciate any design suggestions or similar stuff.
Based on the comment below, I tried to deconstruct trees before grouping results, but it ended outputting the company_id, views_count only, without counting clicks, as following
{ "_id" : "2", "views_count" : 10, "clicks_count" : 0 }
{ "_id" : "1", "views_count" : 6, "clicks_count" : 0 }
$addFields to add company field, check condition if trees.company_id not empty [] then return trees otherwise return links
$arrayElemAt to get first element from array
$group by company_id and sum your counts
db.analytics.aggregate([
{ $lookup: { //... } },
{ $lookup: { //... } },
{
$addFields: {
company: {
$arrayElemAt: [
{ $cond: [{ $ne: ["$trees.company_id", []] }, "$trees", "$links"] },
0
]
}
}
},
{
$group: {
_id: "$company.company_id",
company_name: { $first: "$company.company_name" },
views_count: { $sum: "$views" },
clicks_count: { $sum: "$clicks" }
}
}
])
Playground

Mongo DB filter on other collection without projecting any fields from foreign collection

User Collection
{
"_id: : "123"
"name" : "John Doe",
"age" : 40,
}
Audit collection
{
"_id" : "456",
"region": "IND"
"userId" : 123
}
I need to perform aggregation on User collection where "region" is "IND" but don't want fields from foreign collection to be projected. What I tried so far is this a lookup like below
db.User.aggregate([
{
$lookup:
{
from: "Audit",
localField: "_id",
foreignField: "userId",
as: "auditTrail"
}
},
{
$unwind: "$auditTrail"
},
{
$match : {
"auditTrail.region": "IND"
}
},
{
$project : {"auditTrail.region": 0}
}
])
Other way is to use lookup with pipeline and not project the foreign fields
db.User.aggregate([
{
$lookup:
{
from: "Audit",
let: { user_id: "$_id"},
pipeline: [
{ $match:
{ $expr:
{ $and:
[
{ $eq: [ "$userId", "$$user_id" ] }
]
}
}
},
{ $project: { _id: 1} }
],
as: "stockdata"
}
}
])
Both the collections mentioned here are simplified, in production can be a huge document with thousands of records in each collection where there can be match stage to filter fields on source collection in addition to filtering on foreign collection. Is there any better way to accomplish this?
You can modify your 2nd Query by adding region condition inside $match of $lookup pipeline.
db.User.aggregate([
{
$lookup:
{
from: "Audit",
let: { user_id: "$_id"},
pipeline: [
{ $match:
{ $expr:
{ $and:
[
{ $eq: [ "$userId", "$$user_id" ] },
{ $eq: [ "$region", "IND" ] } // region Condition added
]
}
}
},
{ $project: { _id: 1} }
],
as: "stockdata"
}
}
])

Mongodb combine aggregate queries

I have following collections in MongoDB
Profile Collection
> db.Profile.find()
{ "_id" : ObjectId("5ec62ccb8897af3841a46d46"), "u" : "Test User", "is_del": false }
Store Collection
> db.Store.find()
{ "_id" : ObjectId("5eaa939aa709c30ff4703ffd"), "id" : "5ec62ccb8897af3841a46d46", "a" : { "ci": "Test City", "st": "Test State" }, "ip" : false }, "op" : [ ], "b" : [ "normal" ], "is_del": false}
Item Collection
> db.Item.find()
{ "_id" : ObjectId("5ea98a25f1246b53a46b9e10"), "sid" : "5eaa939aa709c30ff4703ffd", "n" : "sample", "is_del": false}
Relation among these collections are defined as follows:
Profile -> Store: It is 1:n relation. id field in Store relates with _id field in Profile.
Store -> Item: It is also 1:n relation. sid field in Item relates with _id field in Store.
Now, I need to write a query to find the all the store of profiles alongwith their count of Item for each store. Document with is_del as true must be excluded.
I am trying it following way:
Query 1 to find the count of item for each store.
Query 2 to find the store for each profile.
Then in the application logic use both the result to produce the combined output.
I have query 1 as follows:
db.Item.aggregate({$group: {_id: "$sid", count:{$sum:1}}})
Query 2 is as follows:
db.Profile.aggregate([{ "$addFields": { "pid": { "$toString": "$_id" }}}, { "$lookup": {"from": "Store","localField": "pid","foreignField": "id", "as": "stores"}}])
In the query, is_del is also missing. Is there any simpler way to perform all these in a single query? If so, what will be scalability impact?
You can use uncorrelated sub-queries, available from MongoDB v3.6
db.Profile.aggregate([
{
$match: { is_del: false }
},
{
$lookup: {
from: "Store",
as: "stores",
let: {
pid: { $toString: "$_id" }
},
pipeline: [
{
$match: {
is_del: false,
$expr: { $eq: ["$$pid", "$id"] }
}
},
{
$lookup: {
from: "Item",
as: "items",
let: {
sid: { $toString: "$_id" }
},
pipeline: [
{
$match: {
is_del: false,
$expr: { $eq: ["$$sid", "$sid"] }
}
},
{
$count: "count"
}
]
}
},
{
$unwind: "$items"
}
]
}
}
])
Mongo Playground
To improve performance, I suggest you store the reference ids as ObjectId so you don't have to convert them in each step.

mongodb aggregate to find,count and project unique documnets

Below are the sample collection.
col1:
"_id" : ObjectId("5ec293782bc00b43b463b67c")
"status" : ["running"],
"name" : "name1 ",
"dcode" : "dc001",
"address" : "address1",
"city" : "city1"
col2:
"_id" : ObjectId("5ec296182bc00b43b463b68f"),
"scode" : ObjectId("5ec2933df6079743c0a2a1f8"),
"ycode" : ObjectId("5ec293782bc00b43b463b67c"),
"city" : "city1",
"lockedDate" : ISODate("2020-05-20T00:00:00Z"),
"_id" : ObjectId("5ec296182bc00b43b463688b"),
"scode" : ObjectId("5ec2933df6079743c0a2a1ff"),
"ycode" : ObjectId("5ec293782bc00b43b463b67c"),
"city" : "city1",
"lockedDate" : ISODate("2020-05-20T00:00:00Z"),
"_id" : ObjectId("5ec296182bc00b43b44fc6cb"),
"scode" :null,
"ycode" : ObjectId("5ec293782bc00b43b463b67c"),
"city" : "city1",
"lockedDate" : ISODate("2020-05-20T00:00:00Z"),
problemStatement:
I want to display name from col1 & count of documents from col2 according to ycode where scode is != null
Tried attempt:
db.col1.aggregate([
{'$match':{
city:'city1'
}
},
{
$lookup:
{
from: "col2",
let: {
ycode: "$_id",city:'$city'
},
pipeline: [
{
$match: {
scode:{'$ne':null},
lockedDate:ISODate("2020-05-20T00:00:00Z"),
$expr: {
$and: [
{
$eq: [
"$ycode",
"$$ycode"
]
},
{
$eq: [
"$city",
"$$city"
]
}
]
},
},
},
], as: "col2"
}
},
{'$unwind':'$col2'},
{'$count':'ycode'},
{
$project: {
name: 1,
status: 1,
}
},
])
now problem with this query is it either displays the count or project the name & status i.e if i run this query in the current format it gives {} if I remove {'$count':'ycode'} then it project the values but doesn't give the count and if I remove $project then i do get the count {ycode:2} but then project doesn't work but I want to achieve both in the result. Any suggestions
ORM: mongoose v>5, mongodb v 4.0
You can try below query :
db.col1.aggregate([
{ "$match": { city: "city1" } },
{
$lookup: {
from: "col2",
let: { id: "$_id", city: "$city" }, /** Create local variables from fields of `col1` but not from `col2` */
pipeline: [
{
$match: { scode: { "$ne": null }, lockedDate: ISODate("2020-05-20T00:00:00Z"),
$expr: { $and: [ { $eq: [ "$ycode", "$$id" ] }, { $eq: [ "$city", "$$city" ] } ] }
}
},
{ $project: { _id: 1 } } // Optional, But as we just need count but not the entire doc, holding just `_id` helps in reduce size of doc
],
as: "col2" // will be an array either empty (If no match found) or array of objects
}
},
{
$project: { _id: 0, name: 1, countOfCol2: { $size: "$col2" } }
}
])
Test : mongoplayground

MongoDB lookup when foreign field is an array

I've searched the internet and StackOverflow, but I cannot find the answer or even the question.
I have two collections, reports and users. I want my query to return all reports and indicate if the specified user has that report as a favorite in their array.
Reports Collection
{ _id: 1, name:"Report One"}
{ _id: 2, name:"Report Two"}
{ _id: 3, name:"Report Three"}
Users Collection
{_id: 1, name:"Mike", favorites: [1,3]}
{_id: 2, name:"Tim", favorites: [2,3]}
Desired Result for users.name="Mike"
{ _id: 1, name:"Report One", favorite: true}
{ _id: 2, name:"Report Two", favorite: false}
{ _id: 3, name:"Report Three", favorite: true}
All of the answers I can find use $unwind on the local (reports) field, but in this case the local field isn't an array. The foreign field is the array.
How can I unwind the foreign field? Is there a better way to do this?
I saw online that someone suggested making another collection favorites that would contain:
{ _id: 1, userId: 1, reportId: 1 }
{ _id: 2, userId: 1, reportId: 3 }
{ _id: 3, userId: 2, reportId: 2 }
{ _id: 4, userId: 2, reportId: 3 }
This method seems like it should be unnessesary. It should be simple to join onto an ID in a foreign array, right?
You can use $lookup with custom pipeline which will give you 0 or 1 result and then use $size to convert an array to single boolean value:
db.reports.aggregate([
{
$lookup: {
from: "users",
let: { report_id: "$_id" },
pipeline: [
{
$match: {
$expr: {
$and: [
{ $eq: [ "$name", "Mike" ] },
{ $in: [ "$$report_id", "$favorites" ] }
]
}
}
}
],
as: "users"
}
},
{
$project: {
_id: 1,
name: 1,
favorite: { $eq: [ { $size: "$users" }, 1 ] }
}
}
])
Alternatively if you need to use MongoDB version lower than 3.6 you can use regular $lookup and then use $filter to get only those users where name is Mike:
db.reports.aggregate([
{
$lookup: {
from: "users",
localField: "_id",
foreignField: "favorites",
as: "users"
}
},
{
$project: {
_id: 1,
name: 1,
favorite: { $eq: [ { $size: { $filter: { input: "$users", as: "u", cond: { $eq: [ "$$u.name", "Mike" ] } } } }, 1 ] }
}
}
])
"_id" : ObjectId("611fc392cfadfbba65d4f4bd"),
"t_name" : "Bahadur",
"t_age" : "22",
"trch" : "java",
"StudentsDetails" : [
{
"_id" : ObjectId("611fc41ccfadfbba65d4f4be"),
"s_name" : "Asin",
"s_age" : "18",
"trch" : "java",
"tsid" : ObjectId("611fc392cfadfbba65d4f4bd")
},
{
"_id" : ObjectId("611fc8f1a815fb2c737ae31f"),
"s_name" : "sonu",
"s_age" : "18",
"tsid" : ObjectId("611fc392cfadfbba65d4f4bd")
},
{
"_id" : ObjectId("611fc915a815fb2c737ae320"),
"s_name" : "monu",
"s_age" : "19",
"tsid" : ObjectId("611fc392cfadfbba65d4f4bd")
}
]
}
Create Trainer Collection
Create Scholar Collection
//query
db.Trainer.aggregate(
[`enter code here`
{`enter code here`
$lookup:`enter code here`
{`enter code here`
from: "scholar",`enter code here`
localField: "_id",`enter code here`
foreignField: "tsid",`enter code here`
as: "StudentsDetails"`enter code here`
}`enter code here`
}`enter code here`
]`enter code here`
).pretty();