I need to get the data from two collections (expenses, accounts). The data must be between the given date range and amount range.
date, amount, currency, type is in Expenses collection and accountId is in account collection as _id.
I tried the following query but it's not returning anything.
const response = await Expense.aggregate( [
{ $match: {
$and: [
{
currency: "1",
expenseType: "1"
},
{
date: {
$gte: new Date(date.value.from) , $lte: new Date(date.value.to)
},
amount: {
$gt: parseFloat(amount.value.from) , $lt: parseFloat(amount.value.to)
}
}
],
} },
{
$lookup: {
from: "accounts",
localField: "accountId",
foreignField: "_id",
as: "account"
}
}
] )
This is the schema of expenses collection:
Document of expense collection:
Document of account collection:
From the attached expense document, the amount field was a String type but not a Number type.
Data
amount: "20000"
Schema
amount: {
type: Number,
required: true
}
You should revise your Expense document in MongoDB with your schema.
From your Aggregation Query, you need to convert amount from String to Decimal to compare.
Solution 1: With $expr
db.expense.aggregate([
{
$match: {
$and: [
{
currency: "1",
expenseType: "1"
},
{
date: {
$gte: new Date(date.value.from),
$lte: new Date(date.value.to)
},
},
{
$expr: {
$and: [
{
$gt: [
{
"$toDecimal": "$amount"
},
parseFloat(amount.value.from)
]
},
{
$lt: [
{
"$toDecimal": "$amount"
},
parseFloat(amount.value.to)
]
}
]
}
}
],
}
},
{
$lookup: {
from: "accounts",
localField: "accountId",
foreignField: "_id",
as: "account"
}
}
])
Solution 1 on Mongo Playground
Solution 2: Add $set as first stage
db.expense.aggregate([
{
$set: {
amount: {
"$toDecimal": "$amount"
}
}
},
{
$match: {
$and: [
{
currency: "1",
expenseType: "1"
},
{
date: {
$gte: new Date(date.value.from),
$lte: new Date(date.value.to)
},
amount: {
$gt: parseFloat(amount.value.from),
$lt: parseFloat(amount.value.to)
}
}
]
}
},
{
$lookup: {
from: "accounts",
localField: "accountId",
foreignField: "_id",
as: "account"
}
}
])
Solution 2 on Mongo Playground
Related
I have two tables.
The first one is the master collection,
The second collection is the stat collection
Im summing up the recodes in stat table with aggregate, when I'm doing that I want to use the foreign key and get the title from the master collection. Following is the code I have used.
const totalClicks = await StatModel.aggregate([
{ $match: { campaignId: id } },
{
$group: {
_id: { $dateToString: { format: '%Y-%m-%d', date: '$createAt' } },
count: { $sum: 1 },
},
},
{
$lookup: {
from: 'campaign.channels',
localField: 'channel',
foreignField: '_id',
as: 'channel',
},
},
{ $sort: { _id: 1 } },
]);
Output comes as follows
[{"_id":"2022-06-04","count":8,"channel":[]}]
Desired out put is
[{"_id":"2022-06-04","count":8,"channel":"General"}]
You can do like this to get the title from populated channel,
const totalClicks = await StatModel.aggregate([
{ $match: { campaignId: id } },
{
$group: {
_id: { $dateToString: { format: '%Y-%m-%d', date: '$createAt' } },
count: { $sum: 1 },
},
},
{
$lookup: {
from: 'campaign.channels',
localField: 'channel',
foreignField: '_id',
as: 'channel',
},
},
{ $addFields: { channel: { $arrayElemAt: ['$channel', 0] } } },
{ $project: { _id, 1, count: 1, channel: '$channel.title' } },
{ $sort: { _id: 1 } },
]);
I have a collection of users like this
[
{ _id: ObjectId("61a6d586e56ea12d6b63b68e"), fullName: "Mr A" },
{ _id: ObjectId("6231a89b009d3a86c788bf39"), fullName: "Mr B" },
{ _id: ObjectId("6231a89b009d3a86c788bf3a"), fullName: "Mr C" }
]
And a collection of complains like this
[
{ _id: ObjectId("6231aaba2a038b39d992099b"), type: "fee", postedBy: ObjectId("61a6d586e56ea12d6b63b68e" },
{ _id: ObjectId("6231aaba2a038b39d992099b"), type: "fee", postedBy: ObjectId("6231a89b009d3a86c788bf3c" },
{ _id: ObjectId("6231aaba2a038b39d992099b"), type: "fee", postedBy: ObjectId("6231a89b009d3a86c788bf3b" },
]
I want to check if the postedBy fields of complains are not existed in users, then update by using the updateMany query
By the way, I have an optional way to achieve the goal but must use 2 steps:
const complains = await Complain.aggregate()
.lookup({
from: "users",
localField: "postedBy",
foreignField: "_id",
as: "postedBy",
})
.match({
$expr: {
$eq: [{ $size: "$postedBy" }, 0],
},
});
complains.forEach(async (complain) => {
complain.type = "other";
await complain.save();
});
Therefore, can I combine 2 steps into a single updateMany query? Like $match and $lookup inside updateMany query?
With MongoDB v4.2+, you can use $merge to perform update at last stage of aggregation.
db.complains.aggregate([
{
"$lookup": {
from: "users",
localField: "postedBy",
foreignField: "_id",
as: "postedByLookup"
}
},
{
$match: {
postedByLookup: []
}
},
{
"$addFields": {
"type": "other"
}
},
{
"$project": {
postedByLookup: false
}
},
{
"$merge": {
"into": "complains",
"on": "_id",
"whenMatched": "replace"
}
}
])
Here is the Mongo playground for your reference.
I have a Review Model and Product Model such as:
const ProductReviewSchema = Schema({
rating: {
type: Number,
min: 1,
max: 5,
required: true,
},
product: {
type: mongoose.Types.ObjectId,
ref: 'ecommerce-product',
},
});
and
const ProductSchema = Schema({ name: String, price: Number });
and I want to display top-rated product I can do this using find method in reviewschema and using loops
const totalResult = await ProductReviewModel.find({}, 'rating _id');
let averageRatingNumerator = 0;
let averageRatingDenominator = 0;
if (totalResult) {
totalResult.forEach((eachRate) => {
averageRatingDenominator += 1;
averageRatingNumerator += eachRate.rating;
});
}
then find the average rating of each product and sort the result in ascending order. Can I do the exact same thing using mongoose aggregate, If yes what pipeline or operator should I use to achieve such a result:
My Expected result may be is:
[
{ product: 'abc', avgRating: 3.5 },
{ product: 'def', avgRating: 3.1 }
];
Search from the productReview Schema and populate the product,
$group by product and get average of rating
$sort by avgRating in descending order
$lookup and join product collection
$unwind deconstruct product
$project to show required fields
const totalResult = await ProductReviewModel.aggregate([
{
$group: {
_id: "$product",
avgRating: { $avg: "$rating" }
}
},
{ $sort: { avgRating: -1 } },
{
$lookup: {
from: "products", // update your actual product collection name
localField: "_id",
foreignField: "_id",
as: "product"
}
},
{ $unwind: "$product" },
{
$project: {
name: "$product.name",
avgRating: 1
}
}
]).exec();
Playground
Search from product Schema and populate productReview,
$lookup join products review collection
$size to get total reviews
$sum to calculate sum of return array of rating
$let to specify both (total, ratings) as variable
$cond check condition if total is not zero then $divide
$sort by avgRating in descending order
const totalResult = await Products.aggregate([
{
$lookup: {
from: "productsReview", // replace your actual review collection name here
localField: "_id",
foreignField: "product",
as: "review"
}
},
{
$project: {
name: 1,
avgRating: {
$let: {
vars: {
total: { $size: "$review" },
ratings: { $sum: "$review.rating" }
},
in: {
$cond: [{ $eq: ["$$total", 0] }, 0, { $divide: ["$$ratings", "$$total"] }]
}
}
}
}
},
{ $sort: { avgRating: -1 } }
]).exec();
Playground
I have a problem joining two collections in mongoose. I have two collections namely: student and exams.
Student model:
{
fullname: { type: String, required: true },
email: { type: String, required: true },
}
Exams model:
{
test: { type: String, required: false },
top10: [
type: {
studentId: { type: String, required: true },
score: { type: Number, required: false },
}
]
}
Now, I want to join them two by studentId. The result should be:
{
"test": "Sample Test #1",
"students": [
{
"studentId": "5f22ef443f17d8235332bbbe",
"fullname": "John Smith",
"score": 11
},
{
"studentId": "5f281ad0838c6885856b6c01",
"fullname": "Erlanie Jones",
"score": 9
},
{
"studentId": "5f64add93dc79c0d534a51d0",
"fullname": "Krishna Kumar",
"score": 5
}
]
}
What I did was to use aggregate:
return await Exams.aggregate([
{$lookup:
{
from: 'students',
localField: 'top10.studentId',
foreignField: '_id',
as: 'students'
}
}
]);
But this result is not what I had hoped it should be. Any ideas how to achieve this? I would gladly appreciate any help. Thanks!
You can try,
$lookup with students collection
$project to show required fields, $map to iterate loop of top10 array and inside use $reduce to get fullname from students and merge with top10 object using $mergeObjects
db.exams.aggregate([
{
$lookup: {
from: "students",
localField: "top10.studentId",
foreignField: "_id",
as: "students"
}
},
{
$project: {
test: 1,
students: {
$map: {
input: "$top10",
as: "top10",
in: {
$mergeObjects: [
"$$top10",
{
fullname: {
$reduce: {
input: "$students",
initialValue: 0,
in: {
$cond: [
{ $eq: ["$$this._id", "$$top10.studentId"] },
"$$this.fullname",
"$$value"
]
}
}
}
}
]
}
}
}
}
}
])
Playground
Second option you can use $unwind before $lookup,
$unwind deconstruct top10 array
$lookup with students collection
$addFields to convert students array to object using $arrayElemtAt
$group by _id and construct students array and push required fields
db.exams.aggregate([
{ $unwind: "$top10" },
{
$lookup: {
from: "students",
localField: "top10.studentId",
foreignField: "_id",
as: "students"
}
},
{ $addFields: { students: { $arrayElemAt: ["$students", 0] } } },
{
$group: {
_id: "$_id",
test: { $first: "$test" },
students: {
$push: {
studentId: "$top10.studentId",
score: "$top10.score",
fullname: "$students.fullname"
}
}
}
}
])
Playground
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