MongoDB +mongoose join and in query advanced - mongodb

My DB Schema is complicate and I want to make my current query better
In my users collection I have an array groups that has the groups that this user related to.
when the user loggedin
I look for getting all the tasks that:
task is active
current date is between task.fromDate and task.toDate
task isin the group that related to current user
the group.active is true
foreach task that founded I want get the related actions (task) and related responses(user + task).
my DB schema is here
https://mongoplayground.net/p/X9iAEzwDEWa
my current code is running multi queries and I want to make this query better
this is my current code
const {groups} = await User.findOne({ username, active:true })
.select("groups")
.lean()
.exec();
const tasksQuery = {
active: true,
group: { $in: groups },
$or: [
{
fromDate: {
$exists: false,
},
toDate: {
$exists: false,
},
},
{
$expr: {
$and: [
{
$lte: ["$fromDate", "$$NOW"],
},
{
$gte: ["$toDate", "$$NOW"],
},
],
},
},
],
};
const tasks = await Task.find(tasksQuery).lean()
const tasksWithGroup = await Promise.all(
tasks.map(async (task) => {
const group = await Group.findById(task.group).lean().exec();
const actions = await Action.find({task:task._id, active:true}).select("_id").lean().exec();
const numActions = actions?.length
let doneActions = 0
let gradeActions=0
let responses=[]
//get data for student
if(numActions && req.isStudent){
responses = await Response.find({username:req.user ,action:{ $in: actions } }).select(["_id","grade"]).lean().exec()
if(responses.length) {
doneActions = responses.length
gradeActions = responses.filter(c=>c.grade > -1).length
}
}
return { ...task, groupname: group?.name , numActions, actions,doneActions ,gradeActions, responses};
})
);

There are several issues with your scenarios.
scattered collections: MongoDB is not a relational database. $lookup/joining collections can be expensive. You might want to refactor your schema to denormalize and put records that are frequently accessed together in the same collection.
You are relying on application level filtering: you should leverage DB level filtering when possible
You are firing multiple db calls to get data: You can use $lookup to get the data you want in an aggregation pipeline in one single db call.
Without diving deep into schema refactoring, which needs much more context on your actual scenario and will not fit in the focused requirement of a single stackoverflow question, here is a tweaked version of your query:
db.users.aggregate([
{
"$match": {
"_id": {
"$oid": "6390a187bd6b97a4dc58263d"
}
}
},
{
"$lookup": {
"from": "groups",
"localField": "groups",
"foreignField": "_id",
"pipeline": [
{
$match: {
"active": true
}
}
],
"as": "groups"
}
},
{
"$unwind": "$groups"
},
{
"$lookup": {
"from": "tasks",
"localField": "groups._id",
"foreignField": "group",
"pipeline": [
{
$match: {
$expr: {
$and: [
{
active: true
},
{
$gte: [
"$$NOW",
"$fromDate"
]
},
{
$lte: [
"$$NOW",
"$toDate"
]
}
]
}
}
}
],
"as": "tasks"
}
},
{
"$unwind": "$tasks"
},
{
"$lookup": {
"from": "responses",
"localField": "tasks._id",
"foreignField": "task",
"pipeline": [],
"as": "responses"
}
},
{
"$unwind": {
path: "$responses",
preserveNullAndEmptyArrays: true
}
}
])
Mongo Playground
The idea is to link up the collections in $lookup and rely on the sub-pipeline to perform the filtering.

Related

MongoDB Aggregation use value from Match Object in pipelin

i'm using the following aggregation:
const aggregate = [
{
$match: {
mainCatId: new ObjectId(catId),
},
},
{
"$lookup": {
"from": "products",
"pipeline": [
{ "$match": { "subCategory": '$_id' } },
],
"as": "products"
}
},
{ "$unwind": "$products" }
];
The problem is that i have to match the id of each doc in the pipeline section but this is not working. So the question is how can i match the id i"m getting from the match above
Is the syntax below, which uses the traditional syntax for single join conditions, what you are looking for?
const aggregate = [
{
$match: {
mainCatId: new ObjectId(catId),
},
},
{
"$lookup": {
"from": "products",
localField: "subCategory",
foreignField: "_id",
"as": "products"
}
},
{
"$unwind": "$products"
}
]
If not, please provide sample documents and further details about the ways in which your current approach (and this proposed solution) are not working.

Relate and Count Between Two Collections in MongoDB

How can I count the number of completed houses designed by a specific architect in MongoDB?
I have the next two collections, "plans" and "houses".
Where the only relationship between houses and plans is that houses have the id of a given plan.
Is there a way to do this in MongoDB with just one query?
plans
{
_id: ObjectId("6388024d0dfd27246fb47a5f")
"hight": 10,
"arquitec": "Aneesa Wade",
},
{
_id: ObjectId("1188024d0dfd27246fb4711f")
"hight": 50,
"arquitec": "Smith Stone",
}
houses
{
_id: ObjectId
"plansId": "6388024d0dfd27246fb47a5f" -> string,
"status": "under construction",
},
{
_id: ObjectId
"plansId": "6388024d0dfd27246fb47a5f" -> string,
"status": "completed",
}
What I tried was to use mongo aggregations while using $match and $lookup.
The "idea" with clear errors would be something like this.
db.houses.aggregate([
{"$match": {"status": "completed"}},
{
"$lookup": {
"from": "plans",
"pipeline": [
{
"$match": {
"$expr": {
"$and": [
{ "$eq": [ "houses.plansId", { "$toString": "$plans._id" }]},
{ "plans.arquitec" : "Smith Stone" },
]
}
}
},
],
}
}
If it's a single join condition, simply do a project to object ID to avoid any complicated lookup pipelines.
Example playground - https://mongoplayground.net/p/gaqxZ7SzDTg
db.houses.aggregate([
{
$match: {
status: "completed"
}
},
{
$project: {
_id: 1,
plansId: 1,
status: 1,
plans_id: {
$toObjectId: "$plansId"
}
}
},
{
$lookup: {
from: "plans",
localField: "plans_id",
foreignField: "_id",
as: "plan"
}
},
{
$project: {
_id: 1,
plansId: 1,
status: 1,
plan: {
$first: "$plan"
}
}
},
{
$match: {
"plan.arquitec": "Some One"
}
}
])
Update: As per OP comment, added additional match stage for filtering the final result based on the lookup response.

Filter query for fetching product based on categories in MERN application?

This is my Product Schema
const product = mongoose.Schema({
name: {
type: String,
},
category:{
type: ObjectId,
ref: Category
}
}
I want to return the product based on filters coming from the front end.
For example: Lets consider there are 2 categories Summer and Winter. When the user wants to filter product by Summer Category an api call would me made to http://localhost:8000/api/products?category=summer
Now my question is, how do I filter because from the frontend I am getting category name and there is ObjectId in Product Schema.
My attempt:
Project.find({category:categoryName})
If I've understood correctly you can try one of these queries:
First one is using pipeline into $lookup to match by category and name in one stage like this:
yourModel.aggregate([
{
"$lookup": {
"from": "category",
"let": {
"category": "$category",
},
"pipeline": [
{
"$match": {
"$expr": {
"$and": [
{
"$eq": [
"$id",
"$$category"
]
},
{
"$eq": [
"$name",
"Summer"
]
}
]
}
}
}
],
"as": "categories"
}
},
{
"$match": {
"categories": {
"$ne": []
}
}
}
])
Example here
Other way is using normal $lookup and then $filter the array returned by join.
yourModel.aggregate([
{
"$lookup": {
"from": "category",
"localField": "category",
"foreignField": "id",
"as": "categories"
}
},
{
"$match": {
"categories": {
"$ne": []
}
}
},
{
"$set": {
"categories": {
"$filter": {
"input": "$categories",
"as": "c",
"cond": {
"$eq": [
"$$c.name",
"Summer"
]
}
}
}
}
}
])
Example here
Note how both queries uses $match: { categories: { $ne : []}}, this is because if the lookup doesn't find any result it returns an empty array. By the way, this stage is optional.
Also you can add $unwind or $arrayElemAt index 0 or whatever to get only one value from the array.
Also, to do it in mongoose you only need to replace "Summer" with your variable.

How to efficiently recalculate the values of large amounts of data?

I have several collections in MongoDB:
payment
{
"_id":{
"$oid":"6060ded06aa032495d640536"
},
"type":"",
"amount":10,
"createdAt":{
"date":{
"$date":"2021-03-03T16:01:14.137Z"
},
"timestamp":1614787274.137138
},
"finishedAt":{
"date":{
"$date":"2021-03-03T16:13:15.678Z"
},
"timestamp":1614787995.678263
},
"status":true,
"state":"finished",
"destination":{
"identificator":"1234"
}
}
account
{
"_id":{
"$oid":"60677a2c88b356160e415a1e"
},
"name":"",
"providerAccount":{
"identificator":"1234"
},
"targetAmount":0,
"currentAmount":0,
"status":false,
"state":false,
"priority":false,
"createdAt":{
"date":{
"$date":"2021-03-29T00:00:00.000Z"
},
"timestamp":1616976000
}
}
I need to check each payment if it matches the account in the system. If destination.identificator == providerAccount.identificator I need to change the payment type to "internal" and add the payment amount to the currentAmount in the account.
At the moment, I have a python script that does all this by iterating over each payment, but the problem is that there are more than a million such payments, and such a process can take a very long time.
Is there a more efficient way to do this?
You can write two different Aggregation queries which will perform their own lookup operations on the alternating collections and update the values based on logics and conditions.
Note: The execution order is very important for this to work
Note: For both the Aggregation queries, I will be making use of the $merge stage which will work only on MongoDB version >= 4.4
If you are using any earlier versions of MongoDB, loop through the records of the Aggregation results and update the documents manually using PyMongo instead of the $merge pipeline stage which will be the last stage of the Pipelines.
The first query has to be performed on payment collection, which will check if the link exists in the account collection or not.
db.payment.aggregate([
{
"$lookup": {
"from": "account",
"let": {
"invIdentifactor": "$destination.identificator"
},
"pipeline": [
{
"$match": {
"$expr": {
"$eq": [
"$providerAccount.identificator",
"$$invIdentifactor"
],
},
},
},
{
"$project": {
"_id": 1,
},
},
],
"as": "matchedAcc"
}
},
{
"$match": {
"matchedAcc": {
"$ne": []
}
},
},
{
"$project": {
"type": {
"$literal": "internal"
}
},
},
{
"$merge": {
"into": "payment",
"on": "_id",
"whenMatched": "merge",
"whenNotMatched": "discard"
},
},
])
MongoDB Playground sample Execution
Next the Aggregation query for account collection based on "type": "internal" condition added by the previous query.
Note: If there are already documents with "type": "internal" value in payment collection, change type to a different unique key name in the $project stage and update it in the below query and finally unset the key after all the process is done.
db.account.aggregate([
{
"$lookup": {
"from": "payment",
"let": {
"accIdentifactor": "$providerAccount.identificator"
},
"pipeline": [
{
"$match": {
"$expr": {
"$eq": [
"$destination.identificator",
"$$accIdentifactor"
],
},
"type": "internal",
},
},
{
"$group": {
"_id": "$providerAccount.identificator",
"totalAmount": {
"$sum": "$amount"
}
},
},
],
"as": "matchedPayment"
},
},
{
"$match": {
"$expr": {
"$gt": [
{
"$arrayElemAt": [
"$matchedPayment.totalAmount",
0
]
},
0
]
},
},
},
{
"$project": {
"currentAmount": {
"$add": [
"$currentAmount",
{
"$arrayElemAt": [
"$matchedPayment.totalAmount",
0
]
}
]
}
},
},
{
"$merge": {
"into": "account",
"on": "_id",
"whenMatched": "merge",
"whenNotMatched": "discard"
},
},
])
Mongo Playground Sample Execution
Additionally, you can pass the allowDiskUse: true option on Aggregation commands and also consider perform indexing on providerAccount.identificator and destination.identificator keys to speed this up if required and later delete those indexes.
Let me know if you want an explanation of all the stages and operators in the aggregation pipeline.

Mongodb, aggregate query with $lookup

Got two collecetions, tags and persons.
tags model:
{
en: String,
sv: String
}
person model:
{
name: String,
projects: [
title: String,
tags: [
{
type: Schema.ObjectId,
ref: 'tag'
}
]
]
}
I want query that returns all tags that is in use in the person model. All documents.
Sometehing like
var query = mongoose.model('tag').find({...});
Or should I somehow use the aggregate approach to this?
For any particular person document, you can use the populate() function like
var query = mongoose.model("person").find({ "name": "foo" }).populate("projects.tags");
And if you want to search for any persons that have any tag with 'MongoDB' or 'Node JS' for example, you can include the query option in the populate() function overload as:
var query = mongoose.model("person").find({ "name": "foo" }).populate({
"path": "projects.tags",
"match": { "en": { "$in": ["MongoDB", "Node JS"] } }
});
If you want all tags existing in "project.tags" for all persons, then aggregation framework is the way to go. Consider running this pipeline on the person collection and uses the $lookup operator to do a left join on the tags collection:
mongoose.model('person').aggregate([
{ "$unwind": "$projects" },
{ "$unwind": "$projects.tags" },
{
"$lookup": {
"from": "tags",
"localField": "projects.tags",
"foreignField": "_id",
"as": "resultingTagsArray"
}
},
{ "$unwind": "$resultingTagsArray" },
{
"$group": {
"_id": null,
"allTags": { "$addToSet": "$resultingTagsArray" },
"count": { "$sum": 1 }
}
}
]).exec(function(err, results){
console.log(results);
})
For any particular person then apply a $match pipeline as the first step to filter the documents:
mongoose.model('person').aggregate([
{ "$match": { "name": "foo" } },
{ "$unwind": "$projects" },
{ "$unwind": "$projects.tags" },
{
"$lookup": {
"from": "tags",
"localField": "projects.tags",
"foreignField": "_id",
"as": "resultingTagsArray"
}
},
{ "$unwind": "$resultingTagsArray" },
{
"$group": {
"_id": null,
"allTags": { "$addToSet": "$resultingTagsArray" },
"count": { "$sum": 1 }
}
}
]).exec(function(err, results){
console.log(results);
})
Another workaround if you are using MongoDB versions >= 2.6 or <= 3.0 which do not have support for the $lookup operator is to populate the results from the aggregation as:
mongoose.model('person').aggregate([
{ "$unwind": "$projects" },
{ "$unwind": "$projects.tags" },
{
"$group": {
"_id": null,
"allTags": { "$addToSet": "$projects.tags" }
}
}
], function(err, result) {
mongoose.model('person')
.populate(result, { "path": "allTags" }, function(err, results) {
if (err) throw err;
console.log(JSON.stringify(results, undefined, 4 ));
});
});
If you are using MongoDb version 3.2 then you can use $lookup which performs an left outer join.