How to populate an field in array of Mongooese? - mongodb

I have a collection schema like this:
const PrescriptionSchema = new Schema({
patientId:{type: mongoose.Schema.ObjectId, ref: 'User'},
prescriptionName: String,
prescriptionNote: String,
prescriptionDate: Date,
diseaseId: {type: mongoose.Schema.ObjectId, ref: 'Diseases'},
drugs: [{
drugcateId:{type: mongoose.Schema.ObjectId, ref: 'DrugCategory'},
quantity: Number,
howtouse: String
}],
is_deleted: {type: Boolean, default: false},
createdBy: {type: mongoose.Schema.ObjectId, ref: 'User'}
}, {
timestamps: true,
collection: 'prescriptions'
}
);
export default mongoose.model('prescriptions', PrescriptionSchema );
Note that in this collection I have an array of "drugs", this is an array of drug that I get from 'DrugCategory', in each drug I have quantity and howtouse..so that I group it (drugCateId, quantity, howtouse) in drugs array.
When I find a prescription, I want to populate drugCateId inside drugs array. How can I do it?
Below is my current code:
async findOne(req, res){
const { id } = req.params;
await Prescriptions.
findOne({ is_deleted: false, _id: id}).
.populate(
{
path: 'drugs',
populate: {
path: 'drugCateId',
model: 'DrugCategory'
}
}).
exec(function (err, data) {
if (err) return handleError(err);
res.send(data)
});
}
But it's not work.
Below is my result:
{
"is_deleted": false,
"_id": "5f32768a06693a520806717d",
"patientId": "5c80930d447df7735138693e",
"prescriptionName": "Prescription for you",
"prescriptionNote": "Please drink follow doctor",
"prescriptionDate": "2020-07-08T00:00:00.000Z",
"diseaseId": "5f22a6d600980c081ca9e14f",
"drugs": [
{
"_id": "5f32768a06693a5208067181",
"drugcateId": "5f23deca04e3150a48632229", // How can I populate this one?
"quantity": 10,
"howtouse": "drink 2 times after meal everyday"
},
{
"_id": "5f32768a06693a5208067180",
"drugcateId": "5f23deca04e3150a48632233", // How can I populate this one?
"quantity": 10,
"howtouse": "drink 2 times after meal everyday"
},
{
"_id": "5f32768a06693a520806717f",
"drugcateId": "5f23deca04e3150a48632234", // How can I populate this one?
"quantity": 10,
"howtouse": "drink 2 times after meal everyday"
},
{
"_id": "5f32768a06693a520806717e",
"drugcateId": "5f23deca04e3150a4863224a", // How can I populate this one?
"quantity": 10,
"howtouse": "drink 2 times after meal everyday"
}
],
"createdBy": "5d1cd947231ceb95b8838c1b",
"createdAt": "2020-08-11T10:44:26.842Z",
"updatedAt": "2020-08-11T10:44:26.842Z",
"__v": 0
}
I hope you understand my issue, please take a look. Thanks you

You were very close it seems, but make sure to give the proper path:
.populate({
path: 'drugcateId',
model: 'DrugCategory'
})
Change drugCateId to drugcateId.
Also, you should be able to directly populate DrugCategory.

Your current version does not match your schema. It suggest you want to populate both drugs and drugs.drugcateId. But here drugs are embedded directly and not references to documents in another collection, so it's not possible to populate():
.populate({
path: 'drugs', // this assumes drugs are objectIds: [{ type: ObjectId, ref: 'Drug' }]
populate: {
path: 'drugCateId',
model: 'DrugCategory'
}
})
Instead, you should be able to use dot-notation for arrays:
.populate({ path: "drugs.drugcateId" })
or short
.populate("drugs.drugcateId")

Thanks you for your reply. I have a mistake that my model is "drugcate" but in my populate command is "drugCate". I already change my model to "drugCate" and bellow query is worked.
populate({ path: 'drugs.drugCateId',
populate: {path: 'drugs.drugCateId'}
})
Thanks

Related

How can I populate nested array objects in mongoDB or mongoose?

I have an orders collection where each order has the following shape:
{
"_id": "5252875356f64d6d28000001",
"lineItems": [
{ productId: 'prod_007', quantity: 3 },
{ productId: 'prod_003', quantity: 2 }
]
// other fields omitted
}
I also have a products collection, where each product contains a unique productId field.
How can I populate each lineItem.productId with a matching product from the products collection? Thanks! :)
EDIT: orderSchema and productSchema:
const orderSchema = new Schema({
checkoutId: {
type: String,
required: true,
},
customerId: {
type: String,
required: true,
},
lineItems: {
type: [itemSubSchema],
required: true,
},
});
const itemSubSchema = new Schema(
{
productId: {
type: String,
required: true,
},
quantity: {
type: Number,
required: true,
},
},
{ _id: false }
);
const productSchema = new Schema({
productId: {
type: String,
required: true,
},
name: {
type: String,
required: true,
},
imageURL: {
type: String,
required: true,
},
price: {
type: Number,
default: 0,
},
});
I don't know the exact output you want but I think this is what you are looking for:
The trick here is to use $lookup in an aggregation stage.
First $unwind to deconstruct the array and can merge each id with the other collection.
Then the $lookup itself. This is like a join in SQL. It merges the desired objects with same ids.
Then recreate the population using $mergeObjects to get properties from both collections.
And last re-group objects to get the array again.
db.orders.aggregate([
{
"$unwind": "$lineItems"
},
{
"$lookup": {
"from": "products",
"localField": "lineItems.productId",
"foreignField": "_id",
"as": "result"
}
},
{
"$set": {
"lineItems": {
"$mergeObjects": [
"$lineItems",
{
"$first": "$result"
}
]
}
}
},
{
"$group": {
"_id": "$_id",
"lineItems": {
"$push": "$lineItems"
}
}
}
])
Example here
With this query you have the same intial data but "filled" with the values from the other collection.
Edit: You can also avoid one stage, maybe it is clear with the $set stage but this example do the same as it merge the objects in the $group stage while pushing to the array.
You can use the Mongoose populate method either when you query your documents or as middleware. However, Mongoose only allows normal population on the _id field.
const itemSubSchema = new Schema({
product: {
type: mongoose.Schema.Types.ObjectId,
ref: 'productSchema',
}
});
const order = await orderSchema.find().populate('lineItems.$*.product');
// special populate syntax necessary for nested documents
Using middleware you would still need to reconfigure your item schema to save the _id from products. But this method would automatically call populate each time you query items:
itemSubSchema.pre('find', function(){
this.populate('product');
});
You could also declare your item schema within your order schema to reduce one layer of joining data:
const orderSchema = new Schema({
lineItems: [{
type: {
quantity: {type: Number, required: true},
product: {
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: 'productSchema',
}
},
required: true,
}]
});
const orders = orderSchema.find().populate('lineItems');

MongoDB Query from multiple models / schemas and return in one field

I am using Nodejs and MongoDB, mongoose and expressjs, creating a Blog API having users, articles, likes & comments schema. Below are schemas that I use.
const UsersSchema = new mongoose.Schema({
username: { type: String },
email: { type: String },
date_created: { type: Date },
last_modified: { type: Date }
});
const ArticleSchema = new mongoose.Schema({
id: { type: String, required: true },
text: { type: String, required: true },
posted_by: { type: Schema.Types.ObjectId, ref: 'User', required: true },
images: [{ type: String }],
date_created: { type: Date },
last_modified: { type: Date }
});
const CommentSchema = new mongoose.Schema({
id: { type: String, required: true },
commented_by: { type: Schema.Types.ObjectId, ref: 'User', required: true },
article: { type: Schema.Types.ObjectId, ref: 'Article' },
text: { type: String, required: true },
date_created: { type: Date },
last_modified: { type: Date }
});
What I actually need is when I * get collection of articles * I also want to get the number of comments together for each articles. How do I query mongo?
Since you need to query more than one collection, you can use MongoDB's aggregation.
Here: https://docs.mongodb.com/manual/aggregation/
Example:
Article
.aggregate(
{
$lookup: {
from: '<your comments collection name',
localField: '_id',
foreignField: 'article',
as: 'comments'
}
},
{
$project: {
comments: '$comments.commented_by',
text: 1,
posted_by: 1,
images: 1,
date_created: 1,
last_modified: 1
}
},
{
$project: {
hasCommented: {
$cond: {
if: { $in: [ '$comments', '<user object id>' ] },
then: true,
else: false
}
},
commentsCount: { $size: '$comments' },
text: 1,
posted_by: 1,
images: 1,
date_created: 1,
last_modified: 1
}
}
)
The aggregation got a little big but let me try to explain:
First we need to filter the comments after the $lookup. So we $unwind them, making each article contain just one comment object, so we can filter using $match(that's the filter stage, it works just as the <Model>.find(). After filtering the desired's user comments, we $group everything again, $sum: 1 for each comment, using as the grouper _id, the article's _id. And we get the $first result for $text, $images and etc. Later, we $project everything, but now we add hasCommented with a $cond, simply doing: if the $comments is greater than 0(the user has commented, so this will be true, else, false.
MongoDB's Aggregation framework it's awesome and you can do almost whatever you want with your data using it. But be aware that somethings may cost more than others, always read the reference.

Mongoose $push add to document

I'm trying to add an element to an existing array, but it produces an error:
The field 'data' must be an array but is of type object in document
Scheme:
const testScheme = new Schema({
user: {
type: String,
required: true
},
data: [{
platform: {
type: String,
required: true
},
item_name: {
type: String,
required: true
},
price: {
type: Number,
default: 0
},
updatedAt: Date
}]
}, {
versionKey: false,
timestamps: true
});
Document in mongodb:
"data": [{
"price": 50,
"_id": "5a84268d6c78a60c10479437",
"platform": "pl1",
"item_name": "test"
}],
"_id": "5a841bccb44cb8cd5b974d71",
"user": "Ivan",
"updatedAt": "2018-02-14T12:07:41.793Z",
"createdAt": "2018-02-14T11:21:48.104Z"
Query:
var item = {
"platform": "pl700",
"item_name": "someText",
"price": 700,
"updatedAt": new Date()
};
Data.findOneAndUpdate({
'user': 'Ivan'
}, {
$push: {
'data': item
}
}, {
safe: true,
upsert: true
},
function(err, data) {
if (err) return res.status(500).send({
'error': err
});
res.status(200).send({
'data': data
});
}
);
I trying query with $set parametr and it works, but $push, $addToSet didn't work for me. Also i tried to google this problem and can't solve it.
It is not clear what you are intending to do.
To push an item into array you use $addToSet/$push. For updating a array you use $set.
Using $set you can update the whole document or you can update the specific field.
Update whole doc
Data.findOneAndUpdate({
'user': 'Ivan',
'data._id':item._id
}, {
$set: {
'data.$': item
}
}...
)
Update specific field
Data.findOneAndUpdate({
'user': 'Ivan',
'data._id':item._id
}, {
$set: {
'data.$.price': item.price
}
}...
)

How to populate multilevel array in mongoose

I have a Quiz Model with array of Section and each Sections has array of reference to Question.
I want to populate Quiz with question.
QuizModel
var quizSchema = new Schema({
name: String,
code: Number,
creator: String,
createdBy: { type: Schema.Types.ObjectId, ref: 'user' },
section:[{
title: String,
questions: [{type: Schema.Types.ObjectId ,ref: 'question'}]
}],
createdOn: {type:Date, default:Date.now}
});
and questionModel
var questionSchema = new mongoose.Schema(
{
question: String,
answer: [String],
correct: Number,
createdOn: {type:Date, default:Date.now}
});
I have following the official mongoose documentation for populating
http://mongoosejs.com/docs/populate.html#deep-populate
My attempt
quizSchema.find({_id: quiz_id})
.populate({
path: 'section' ,
populate: {
path: 'section.questions',
model: 'questionSchema'
}
})
.exec( function (err, result) {
if (err) return done(err,null);
console.log("list of questions are" + result);
return done(err, result);
});
}
The output I am getting is list of question's id not the actual question.
{
"status": "success",
"message": "Quiz data",
"result": [
{
"_id": "57fd5912ec0ad6bc8b67d71c",
"name": "My Quiz",
"creator": "foo",
"__v": 0,
"code": 124,
"createdOn": "2016-10-11T21:26:42.774Z",
"section": [
{
"_id": "57fd7e82c20a2fe5da3ed569",
"questions": [
"57fd7f8560e98fe710878820",
"57fd7f9d60e98fe710878821",
"57fd81408b20dae9108d347c",
"57fd81408b20dae9108d347d",
"57fd826aea5159ea5ff2f1a9",
"57fd82ab0dbc0feaa753e50c",
"57fd82efd789afeb0353f036",
"57fd84b0fef6a2ed21fad5ae",
"57fd84cc5dab10ed471bcaf5",
"57fd84cd5dab10ed471bcaf6"
]
},
{
"title": "next section",
"_id": "57fff1e0f1913138c27e50a0",
"questions": [
"57fff242f1913138c27e50a1"
]
}
]
}
]
}
I think I am doing something wrong with populate field, but not sure .
Just alter your query to
quizSchema.find({_id: quiz_id})
.populate({
path: 'section.questions'
})

auto increment ids in mongoose

How do I have autoincrement ids in mongoose? I want my ids to start like 1, 2, 3, 4, not the weird id numbers mongodb creates for you?
Here's my schema:
var PortfolioSchema = mongoose.Schema({
url: String,
createTime: { type: Date, default: Date.now },
updateTime: { type: Date, default: Date.now },
user: {type: Schema.Types.ObjectId, ref: 'User'}
});
Use mongoose-auto-increment:
https://github.com/codetunnel/mongoose-auto-increment
var mongoose = require('mongoose');
var autoIncrement = require('mongoose-auto-increment');
var connection = ....;
autoIncrement.initialize(connection);
var PortfolioSchema = new mongoose.Schema({
url: String,
createTime: { type: Date, default: Date.now },
updateTime: { type: Date, default: Date.now },
user: {type: Schema.Types.ObjectId, ref: 'User'}
});
//Auto-increment
PortfolioSchema.plugin(autoIncrement.plugin, { model: 'Portfolio' });
module.exports = mongoose.model('Portfolio', PortfolioSchema);
Or if you prefer to use an additional field instead of overriding _id, just add the field and list it in the auto-increment initialization:
var PortfolioSchema = new mongoose.Schema({
portfolioId: {type: Number, required: true},
url: String,
createTime: { type: Date, default: Date.now },
updateTime: { type: Date, default: Date.now },
user: {type: Schema.Types.ObjectId, ref: 'User'}
});
//Auto-increment
PortfolioSchema.plugin(autoIncrement.plugin, { model: 'Portfolio', field: 'portfolioId' });
If you want to have a incrementing numeric value in _id then the basic process is you are going to need something to return that value from a store somewhere. One way to do this is use MongoDB itself to store data that holds the counters for the _id values for each collection, which is described within the manual itself under Create and Auto-Incrementing Sequence Field.
Then as you create each new item, you use the implemented function to get that "counter" value, and use it as the _id in your document.
When overriding the default behavior here, mongoose requires that you both specify the _id and it's type explicitly with something like _id: Number and also that you tell it to no longer automatically try to supply an ObjectId type with { "_id": false } as an option on the schema.
Here's a working example in practice:
var async = require('async'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
mongoose.connect('mongodb://localhost/test');
var counterSchema = new Schema({
"_id": String,
"counter": { "type": Number, "default": 1 }
},{ "_id": false });
counterSchema.statics.getNewId = function(key,callback) {
return this.findByIdAndUpdate(key,
{ "$inc": { "counter": 1 } },
{ "upsert": true, "new": true },
callback
);
};
var sampleSchema = new Schema({
"_id": Number,
"name": String
},{ "_id": false });
var Counter = mongoose.model( 'Counter', counterSchema ),
ModelA = mongoose.model( 'ModelA', sampleSchema ),
ModelB = mongoose.model( 'ModelB', sampleSchema );
async.series(
[
function(callback) {
async.each([Counter,ModelA,ModelB],function(model,callback) {
model.remove({},callback);
},callback);
},
function(callback) {
async.eachSeries(
[
{ "model": "ModelA", "name": "bill" },
{ "model": "ModelB", "name": "apple" },
{ "model": "ModelA", "name": "ted" },
{ "model": "ModelB", "name": "oranage" }
],
function(item,callback) {
async.waterfall(
[
function(callback) {
Counter.getNewId(item.model,callback);
},
function(counter,callback) {
mongoose.model(item.model).findByIdAndUpdate(
counter.counter,
{ "$set": { "name": item.name } },
{ "upsert": true, "new": true },
function(err,doc) {
console.log(doc);
callback(err);
}
);
}
],
callback
);
},
callback
);
},
function(callback) {
Counter.find().exec(function(err,result) {
console.log(result);
callback(err);
});
}
],
function(err) {
if (err) throw err;
mongoose.disconnect();
}
);
For convience this implements a static method on the model as .getNewId() which just descriptively wraps the main function used in .findByIdAndUpdate(). This is a form of .findAndModify() as mentioned in the manual page section.
The purpose of this is that it is going to look up a specific "key" ( actually again the _id ) in the Counter model collection and perform an operation to both "increment" the counter value for that key and return the modified document. This is also aided with the "upsert" option, since if no document yet exists for the requested "key", then it will be created, otherwise the value will be incremented via $inc, and it always is so the default will be 1.
The example here shows that two counters are being maintained independently:
{ _id: 1, name: 'bill', __v: 0 }
{ _id: 1, name: 'apple', __v: 0 }
{ _id: 2, name: 'ted', __v: 0 }
{ _id: 2, name: 'oranage', __v: 0 }
[ { _id: 'ModelA', __v: 0, counter: 2 },
{ _id: 'ModelB', __v: 0, counter: 2 } ]
First listing out each document as it is created and then displaying the end state of the "counters" collection which holds the last used values for each key that was requested.
Also note those "weird numbers" serves a specific purpose of always being guranteed to be unique and also always increasing in order. And note that they do so without requiring another trip to the database in order to safely store and use an incremented number. So that should be well worth considering.