How can I combine multiple Mongodb queries into one using node.js? - node.js

Context:
My database has two collections: "Users" and "Files".
Sample document from "Users" collection:
{
"username":"Adam",
"email":"adam#gmail.com",
"IdFilesOwned":[1,3],
}
As you can see, Adam currently owns two files on the server. Their ids are 1 and 3.
Sample documents from "Files" collection:
{
"fileId":1,
"name":"randomPNG.png"
}
{
"fileId":2,
"name":"somePDF.pdf"
}
{
"fileId":3,
"name":"business.pdf"
}
As you can see, I have three documents on my server, each document having an id and some metadata.
Now, Adam wants to see all the files that he owns, and their metadata. The way i would implement this, is:
1.Lookup the array of file ids that Adam owns.
2.Have node.js run through each id (using a for each loop), and query the metadata for each id.
The problem is that nodejs will make multiple queries (1 query per id). This seems very inefficient. So my question is if there is a better way to implement this?

You can use the $in operator find more info here
So it will look something like this.
db.collection('files').find({ fileId: { $in: [<value1>, <value2>, ... <valueN> ] } })
This is more efficent than lookup for sure. Good luck.

I don't have much experience using the driver directly, but you should be able to do the equivalent of the following
db.users.aggregate([
{
$lookup: {
from: "files",
localField: "IdFilesOwned",
foreignField: "fileId",
as: "files"
}
}
]);

Related

MongoDB - Mongoose with NodeJS find and join collection

I need to find and join another collection to get the businesses data from businesses collection and the profile description which is saved in the profiles collection. Latest version of nodejs and mongoose.
businesses = await Business.find({}, "business_id name industry")
.limit(limit * 1)
.skip((page - 1) * limit)
.exec();
That is the code, which I need later also for the pagination.
Now I found a solution with $Lookup in Mongoose. My code looks like
Business.aggregate([{
$lookup: {
from: "profiles", // collection name in db
localField: "business_id",
foreignField: "business_id",
as: "profile"
}
}]).exec(function(err, profile) {
console.log(profile[0]);
});
The Business and Profile is saved with the business_id in a field. So I can't work with _id from Mongoose. I never before work with mongoose and two collections.
The issue is now that the profile[0] is not connected to the correct business. So the profile is a another one as from the business find above.
I need to find the latest 10 Businesses and join to another collection and grap also the profile details. What I make wrong here, has anyone a example for this behauivor ?
Use https://mongoosejs.com/docs/populate.html
As in your case you don't have ObjectId here you can use populate-virtuals
So far you've only populated based on the _id field. However, that's sometimes not the right choice. In particular, arrays that grow without bound are a MongoDB anti-pattern. Using mongoose virtuals, you can define more sophisticated relationships between documents.
const BusinessSchema = new Schema({
name: String
});
BusinessSchema.virtual('profile', {
ref: 'Profile', // The model to use
localField: 'business_id', // Find people where `localField`
foreignField: 'business_id', // is equal to `foreignField`
// If `justOne` is true, 'members' will be a single doc as opposed to
// an array. `justOne` is false by default.
justOne: false,
options: { sort: { name: -1 }, limit: 5 } // Query options, see "bit.ly/mongoose-query-options"
});

How can I make a query like "array that contain no other values than" in mongodb?

I hope I don't miss something obvious, but I haven't seen anywhere a syntax like that.
I'm working for an online courses provider where the user subscribes by topic.
A course can have multiple topics
User must have subscribed for all topics of that course to have access to it
I know how to make a request like { topics: { $nin : ...allTheTopicsUserHaveNoAccessTo } } but that forces me to get all "not allowed" topics first.
Is there a way to make that kind of request in one call ?
User model:
const userModel = {
allowedTopics: [ 'math', 'physics' ],
}
Course model:
const courseModel = {
topics: [ 'math', 'physics', 'biology' ], // user need to have subscribed to those three to see it
}
setIsSubset does it in a single command and can be used in a regular find with $expr operator:
db.course.find({
$expr: {
$setIsSubset: [
"$topics",
user.allowedTopics
]
}
})
https://mongoplayground.net/p/JfSiSrboOuj
It still won't let you benefit from multikey indexes though.
Yes it can be done.
Aggregation with $setDifference followed by a match for empty array will accomplish that.
However, such an aggregation pipeline would not be able to make use of an index, and would have to fetch from disk and examine every course in the entire catalog for every single run.

Create View from multiple collections MongoDB

I have following Mongo Schemas(truncated to hide project sensitive information) from a Healthcare project.
let PatientSchema = mongoose.Schema({_id:String})
let PrescriptionSchema = mongoose.Schema({_id:String, patient: { type: Number, ref: 'Patient', createdAt:Date }})
let ReportSchema = mongoose.Schema({_id:String, patient: { type: Number, ref: 'Patient', createdAt:Date }})
let EventsSchema = mongoose.Schema({_id:String, patient: { type: Number, ref: 'Patient', createdAt:Date }})
There is ui screen from the mobile and web app called Health history, where I need to paginate the entries from prescription, reports and events sorted based on createAt. So I am building a REST end point to get this heterogeneous data. How do I achieve this. Is it possible to create a "View" from multiple schema models so that I won't load the contents of all 3 schema to fetch one page of entries. The schema of my "View" should look like below so that I can run additional queries on it (e.g. find last report)
{recordType:String,/* prescription/report/event */, createdDate:Date, data:Object/* content from any of the 3 tables*/}
I can think of three ways to do this.
Imho the easiest way to achieve this is by using an aggregation something like this:
db.Patients.aggregate([
{$match : {_id: <somePatientId>},
{
$lookup:
{
from: Prescription, // replicate this for Report and Event,
localField: _id,
foreignField: patient,
as: prescriptions // or reports or events,
}
},
{ $unwind: prescriptions }, // or reports or events
{ $sort:{ $createDate : -1}},
{ $skip: <positive integer> },
{ $limit: <positive integer> },
])
You'll have to adapt it further, to also get the correct createdDate. For this, you might want to look at the $replaceRoot operator.
The second option is to create a new "meta"-collection, that holds your actual list of events, but only holds a reference to your patient as well as the actual event using a refPath to handle the three different event types. This solution is the most elegant, because it makes querying your data way easier, and probably also more performant. Still, it requires you to create and handle another collection, which is why I didn't want to recommend this as the main solution, since I don't know if you can create a new collection.
As a last option, you could create virtual populate fields in Patient, that automatically fetch all prescriptions, reports and events. This has the disadvantage that you can not really sort and paginate properly...

Is there any feature available to access a aggregate count function in Mongoose?

I'm new to Node.js and Mongoose library. My problem is i have two collections schema
Restaurant
Reviews
I tried to add a virtual field (reviews_count) in Restaurent collection for reviews count.
How can I achieve this one? Is there any specific function available in mongoose?
Restaurant: Reviews:
_id: ObjectId restaurent_id: (Restaurant ref id)
name: String review: String
I expect the output while try to get the restaurant details.
{
"_id": "2344....",
"name": "Restaurant name".
"reviews_count": 220
}
yes, this is possible. There are many ways to do it: You could actually define an async virtual, that will query Reviews, but I don't like this approach too much, since you have to make sure to await it or to handle a callback, which looks and smells bad:
const count = await restaurant.reviews
Alternatively, you could just define an instance method something like this (async would be better, tough), which I like better, since it makes the code easier to read, when calling this function:
// assign a function to the "methods" object of your restaurant schema
restaurant.methods.getReviews = function(cb) {
return this.model('Reviews').find({ restaurant_id: this._id }, cb);
};
Finally, there is a third, and in my opinion the best option: Virtual Populate
Mongoose 4.5 introduced this feature and it perfectly fits your usecase. Define your field like this:
Restaurant.virtual('reviews', {
ref: 'Reviews',
localField: '_id',
foreignField: 'restaurant_id'
});
and then query it like this:
Restaurant.findOne().populate('reviews').exec(function(error, reviews) {
// `reviews.count` is the virtual you are looking for
});
This is imho the cleanest and most versatile solution since it keeps you from unnecessarily adding arrays with refs to you your main object and still get an easy access to all reviews of a restaurant. You can find a good article here:
http://thecodebarbarian.com/mongoose-virtual-populate
Note: This solution might give you performance problems, since we always query the whole review document and not just the count. If you feel like this might be a problem, you might want to first add the .lean() function to your query, and if that doesn't help much, you can resort to one of the first two approaches I outlined.

Query/Find MongoDB documents with series of multiple conditions

I have a User schema with basic fields which include interests, location co-ordinates
I need to perform POST request with a specific UserId to get the results
app.post('api/users/search/:id',function(err,docs)
{ //find all the documents whose search is enabled.
//on documents returned in above find the documents who have atleast 3 common interests(req.body.interests) with the user with ':id'
// -----OR-----
//find the documents who stay within 'req.body.distance' compared to location of user with':id'
//Something like this
return User
.find({isBuddyEnabled:true}),
.find({"interests":{"$all":req.body.interests}}),
.find({"_id":req.params.id},geoLib.distance([[req.body.gcordinates],[]))
});
Basically i need to perform find inside find or Query inside query..
As per your comments in the code you want to use multiple conditions in your find query such that either one of those condition is satisfied and returns the result based on it. You can use $or and $and to achieve it. A sample code with conditions similar to yours is given below.
find({
$or:[
{ isBuddyEnabled:true },
{ "interests": { "$all":req.body.interests }},
{ $and:[
{ "_id":req.params.id },
{ geoLib.distance...rest_of_the_condition }
]
}
]
});

Resources