mongodb query agregate map many to many - node.js

I have two collections:
Courses
Students
I want to get details of students that are assigned to courses in related courses object.
E.g here are my collections and data
Courses:
[
{
_id: 1,
title: "Maths",
studentGroups: [
{
group: 'A',
students: [1, 5]
},
{
group: 'B',
students: [3]
}
]
},
{
_id: 2,
title: "Chemistry",
studentGroups: [
{
group: 'C',
students: [2]
},
{
group: 'B',
students: [4]
}
]
}
]
Students:
[
{
_id: 1,
name: 'Henry',
age: 15
},
{
_id: 2,
name: 'Kim',
age: 20
},
{
_id: 3,
name: 'Michel',
age: 14
},
{
_id: 4,
name: 'Max',
age: 16
},
{
_id: 5,
name: 'Nathan',
age: 19
}
]
Now I want this response:
[
{
_id: 1,
title: "Maths",
studentGroups: [
{
group: 'A',
students: [
{
_id: 1,
name: 'Henry',
age: 15
},
{
_id: 5,
name: 'Nathan',
age: 19
}
]
},
{
group: 'B',
students: [
{
_id: 3,
name: 'Michel',
age: 14
}
]
}
]
},
{
_id: 2,
title: "Chemistry",
studentGroups: [
{
group: 'C',
students: [
{
_id: 2,
name: 'Kim',
age: 20
}
]
},
{
group: 'B',
students: [
{
_id: 4,
name: 'Max',
age: 16
}
]
}
]
}
]
What I have tried gives my the correct result but it is not a good approach, I need to achieve this by single aggregation query
My solutions:
var courses = Courses.find({}).fetch();
courses.forEach(c => {
if (c.studentGroups && c.studentGroups.length) {
c.studentGroups.forEach(s => {
s.students = Students.find({_id: {$in: s.students}}).fetch()
})
}
})
Can anyone suggest me a better solution with single aggregation query?

First $unwind the studentGroups. Then, $lookup from students. Finally, regroup the results using $group`.
db.courses.aggregate([
{
"$unwind": "$studentGroups"
},
{
"$lookup": {
"from": "students",
"localField": "studentGroups.students",
"foreignField": "_id",
"as": "studentGroups.students"
}
},
{
$group: {
_id: "$_id",
title: {
$first: "$title"
},
studentGroups: {
$push: "$studentGroups"
},
description: {
$first: "$description"
},
timeline: {
$first: "$timeline"
}
}
}
])
Mongo Playground

Related

How to create mongodb aggregation pipeline between two collections?

I want to create a Mongodb aggregation pipeline for a collection named Transaction.
The Transaction collection has values amount, categoryID, description and I also have a Category collection with values type, icon and color.
I want the pipeline to show the top3 categories with their percentage values and a others category with its percentage value.
the transaction type should be Expense which it should get from the Category collection and it should show all transactions having category type Expense. The top3 should then give the results as transaction with category (example)
type : Rent
percentage:45
type: Entertainment
percentage: 30
type: Food
percentage: 20
type: Others
percentage: 5
I tried it with Category collection but I don't want category to store amount, but Transaction should store amount.
Category.aggregate([
{
$match: {
type: 'expense'
}
},
{
$group: {
_id: "$name",
amount: { $sum: "$amount" }
}
},
{
$group: {
_id: null,
totalExpense: { $sum: "$amount" },
categories: {
$push: {
name: "$_id",
amount: "$amount"
}
}
}
},
{
$project: {
_id: 0,
categories: {
$map: {
input: "$categories",
as: "category",
in: {
name: "$$category.name",
percent: { $multiply: [{ $divide: ["$$category.amount", "$totalExpense"] }, 100] }
}
}
}
}
},
{
$unwind: "$categories"
},
{
$sort: { "categories.percent": -1 }
},
{
$limit: 3
}
])
This was the pipeline I used for it.
//edit
Tried the method suggested by Joe
Transaction.aggregate([
// Join the Transaction collection with the Category collection
{
$lookup: {
from: 'Category',
localField: 'categoryID',
foreignField: '_id',
as: 'category',
},
},
// Unwind the category array to separate documents
{
$unwind: '$category',
},
// Filter for transactions where the category type is "Expense"
{
$match: {
'category.type': 'Expense',
},
},
// Group transactions by category type and calculate the percentage
{
$group: {
_id: '$category.type',
total: { $sum: '$amount' },
count: { $sum: 1 },
},
},
{
$project: {
_id: 0,
category: '$_id',
percentage: {
$multiply: [{ $divide: ['$count', { $sum: '$count' }] }, 100],
},
},
},
// Sort the categories by percentage in descending order
{
$sort: { percentage: -1 },
},
// Limit the result to top 3 categories
{
$limit: 3,
},
// group the rest of the categories as others
{
$group: {
_id: null,
top3: { $push: '$$ROOT' },
others: { $sum: { $subtract: [100, { $sum: '$top3.percentage' }] } },
},
},
{
$project: {
top3: 1,
others: { category: 'Others', percentage: '$others' },
},
},
]);
I am getting an empty array rather than the values. I have data in the collections with the correct ID's. What might be the issue?
//Answer
This aggregation worked for me
Transaction.aggregate([
{
$match: {
userID: { $eq: UserID },
type: 'Expense',
},
},
{
$addFields: { categoryID: { $toObjectId: '$categoryID' } },
},
{
$lookup: {
from: 'categories',
localField: 'categoryID',
foreignField: '_id',
as: 'category_info',
},
},
{
$unwind: '$category_info',
},
{
$group: {
_id: '$category_info.name',
amount: { $sum: '$amount' },
},
},
{
$sort: {
amount: -1,
},
},
{
$group: {
_id: null,
total: { $sum: '$amount' },
data: { $push: '$$ROOT' },
},
},
{
$project: {
results: {
$map: {
input: {
$slice: ['$data', 3],
},
in: {
category: '$$this._id',
percentage: {
$round: {
$multiply: [{ $divide: ['$$this.amount', '$total'] }, 100],
},
},
},
},
},
others: {
$cond: {
if: { $gt: [{ $size: '$data' }, 3] },
then: {
amount: {
$subtract: [
'$total',
{
$sum: {
$slice: ['$data.amount', 3],
},
},
],
},
percentage: {
$round: {
$multiply: [
{
$divide: [
{
$subtract: [
'$total',
{ $sum: { $slice: ['$data.amount', 3] } },
],
},
'$total',
],
},
100,
],
},
},
},
else: {
amount: null,
percentage: null,
},
},
},
},
},
]);

mongoose - how to query records of a collection which combine with an ObjectId-to-Number Map from another collection?

Consider the schema below:
schema/collectible.ts: The items that can be collected
const Collectible = new mongoose.schema({
title: String,
image: String
})
new Collectible({ title: 'ABC', image: 'ABC.png' }).save()
new Collectible({ title: 'DEF', image: 'DEF.png' }).save()
schema/inventory.ts: Storing a Map that represent the number of Collectible that an user owned
const Inventory = new mongoose.schema({
owner: { type: Types.ObjectId, ref: 'User' },
items: {
type: Map,
of: Number
}
})
new Inventory({
owner: ObjectId(USER_ID),
items: {
"abc_objectid_str": 2,
"def_objectid_str": 4,
}
}).save()
The goal of the query result is
[
{ title: 'ABC', image: 'ABC.png', amount: 2 },
{ title: 'DEF', image: 'DEF.png', amount: 4 },
]
One option is to use $lookup pipeline with $objectToArray and $reduce:
db.collectible.aggregate([
{$lookup: {
from: "inventory",
let: {title: {$toLower: "$title"}},
pipeline: [
{$match: {owner: ObjectId("5a934e000102030405000000")}},
{$project: {_id: 0, data: {$objectToArray: "$items"}}},
{$set: {
data: {
$reduce: {
input: "$data",
initialValue: 0,
in: {
$add: [
{$cond: [
{$eq: [{$first: {$split: ["$$this.k", "_"]}}, "$$title"]},
"$$this.v",
0
]},
"$$value"
]
}
}
}
}
}
],
as: "countData"
}
},
{$project: {_id: 0, image: 1, title: 1, amount: {$first: "$countData.data"}}}
])
See how it works on the playground example

I am trying to get retrive data from mongodb but not getting expected output

DB Data -
[{
title: "Vivo X50",
category: "mobile",
amount: 35000
},
{
title: "Samsung M32",
category: "mobile",
amount: 18000
},
{
title: "Lenovo 15E253",
category: "laptop",
amount: 85000
},
{
title: "Dell XPS 15R",
category: "laptop",
amount: 115000
}]
Expected Output:
[{
category: "mobile",
qty: 2,
totalAmount: 53000
},
{
category: "laptop",
qty: 2,
totalAmount: 200000
}]
Code I am running (Using mongoose)
let products = await Product.aggregate([
{
$project: { _id: 0, category: 1, amount: 1 },
},
{
$group: {
_id: "$category",
qty: { $sum: 1 },
totalAmount: { $sum: "$amount" },
},
},
]);
Result I am Getting.
[
{
"_id": "laptop",
"count": 2,
"totalSum": 200000
},
{
"_id": "mobile",
"count": 2,
"totalSum": 53000
}
]
As you can clearly see that I am able to get correct data but I want correct name also category instead of _id. Please help me with that. Thanks in advance
You need $project as the last stage to decorate the output document.
{
$project: {
_id: 0,
category: "$_id",
qty: "$qty",
totalAmount: "$totalAmount"
}
}
Meanwhile, the first stage $project doesn't need.
db.collection.aggregate([
{
$group: {
_id: "$category",
qty: {
$sum: 1
},
totalAmount: {
$sum: "$amount"
}
}
},
{
$project: {
_id: 0,
category: "$_id",
qty: "$qty",
totalAmount: "$totalAmount"
}
}
])
Sample Mongo Playground
You can use the following query to get your expected output. cheers~
await Product.aggregate([
{
$group: {
_id: "$category",
qty: {
$sum: 1
},
totalAmount: {
$sum: "$amount"
},
},
},
{
$addFields: {
category: "$_id"
}
},
{
$project: {
_id: 0
},
}
])

How to use aggregate to group mongodb

I'm trying to populate my user document with his talents and talent media. But I'm getting repeated objects. I want to get talent as an object and the medias against that talent inside an array.
my user model:
{
_id: '5f1acd6e6985114f2c1567ea',
name: 'test user',
email: 'test#email.com'
}
talent model
{
_id: '5f1acd6e6985114f2c1567fa',
categoryId: '5f1acd6e6985114f2c1567ga',
userId: '5f1acd6e6985114f2c1567ea'
level: '5',
}
talent-media model
{
_id: 5f1acd6e6985114f2c156710',
talentId: '5f1acd6e6985114f2c1567fa',
media: 'file.jpg',
fileType: 'image'
}
I have another model for storing the category
{
_id: '5f1acd6e6985114f2c1567ga',
title: 'java'
}
I want the result as follows
user: {
_id: '',
name: 'test user',
email: 'test#email.com',
talents: [
{
_id: '',
level: '5',
categoryId: {
_id: '',
title: 'java'
},
medias: [
{
_id: '',
file: 'file1.jpg',
fileType: 'image'
},
{
_id: '',
file: 'file2.jpg',
fileType: 'image'
},
]
}
]
}
And I also tried adding talent-medias embedded in talent documents. But in MongoDB document found is not mostly recommended.
Is it better to have talent model like this,
{
_id: '5f1acd6e6985114f2c1567fa',
categoryId: '5f1acd6e6985114f2c1567ga',
userId: '5f1acd6e6985114f2c1567ea'
level: '5',
medias: [
{
_id: '',
file: 'file1.jpg',
fileType: 'image'
},
{
_id: '',
file: 'file2.jpg',
fileType: 'image'
},
]
}
You can achieve this by using $lookup multiple times:
db.users.aggregate([
{
$lookup: {
from: "talents",
let: {
userId: "$_id"
},
pipeline: [
{
$match: {
$expr: {
$eq: [
"$$userId",
"$userId"
]
}
}
},
{
$lookup: {
from: "categories",
let: {
categoryId: "$categoryId"
},
pipeline: [
{
$match: {
$expr: {
$eq: [
"$$categoryId",
"$_id"
]
}
}
},
{
$project: {
_id: "",
title: 1
}
}
],
as: "categoryId"
}
},
{
$lookup: {
from: "talent_media",
let: {
talentId: "$_id"
},
pipeline: [
{
$match: {
$expr: {
$eq: [
"$$talentId",
"$talentId"
]
}
}
},
{
$project: {
_id: "",
file: "$media",
fileType: "$fileType"
}
}
],
as: "medias"
}
},
{
$unwind: {
path: "$categoryId",
preserveNullAndEmptyArrays: true
}
},
{
$project: {
_id: "",
categoryId: 1,
level: 1,
medias: 1
}
}
],
as: "talents"
}
},
{
$project: {
_id: "",
email: 1,
name: 1,
talents: 1
}
}
])
*You'll probably have to adapt the collection names.
MongoPlayground
You have to use nested $lookup,
lookup with Talent Collection and match userId
$match for specific user _id document, commented here
db.User.aggregate([
/** add here
Optional for single user
{
$match: {
_id: "XXXXXXXXXXXX"
}
},
*/
{
$lookup: {
from: "Talent",
as: "talents",
let: {
userId: "$_id"
},
pipeline: [
{
$match: {
$expr: {
$eq: [
"$$userId",
"$userId"
]
}
}
},
we are now inside Talent Document and lookup with Category Collection and match with categoryId
$unwind category because it will return array and we need object
{
$lookup: {
from: "Category",
as: "categoryId",
localField: "categoryId",
foreignField: "_id"
}
},
{
$unwind: {
path: "$categoryId",
preserveNullAndEmptyArrays: true
}
},
again we are inside Talent Document and lookup with Talent Media Collection and match with talentId
$project for add/remove not needed fields, here removed talentID from TalentMedia nad removed userId from Talent
{
$lookup: {
from: "TalentMedia",
as: "medias",
let: {
talentId: "$_id"
},
pipeline: [
{
$match: {
$expr: {
$eq: [
"$$talentId",
"$talentId"
]
}
}
},
{
$project: {
talentId: 0
}
}
]
}
},
{
$project: {
userId: 0
}
}
]
}
}
])
Separated query in parts for explanation, You can combine as they are in sequence.
Working Playground: https://mongoplayground.net/p/poIbvFUMvT4

How to aggregate for sum the results and group by given field?

I have documents like this
items = [{
owner: [{ name: "user1" }, { name: "user2" }]
},
{
owner: [{ name: "user1" }, { name: "user2" }]
},
{
owner: [{ name: "user2" }, { name: "user3" }, { name: "user4" }]
}];
So I need to count a number of the same name and group by name and rank them like this
results = [{ name: "user2", count: 3 },
{ name: "user1", count: 2 },
{ name: "user3", count: 1 },
{ name: "user4", count: 1 }];
You can use below aggregation:
db.collection.aggregate([
{
$unwind: "$owner"
},
{
$group: {
_id: "$owner.name",
count: { $sum: 1 }
}
},
{
$project: {
_id: 0,
name: "$_id",
count: 1
}
},
{
$sort: {
count: -1
}
}
])
$unwind can be used to get single name per document, then you need $group to count and $sort for ranking
Mongo Playground

Resources