How to populate with a second model if first one is empty - mongodb

I've three models,
modelA, modelB and ModelC
ModelC's data is here
{
"_id" : ObjectId("586e1661d9903c6027a3b47c"),
"RefModel" : "modelA",
"RefId" : ObjectId("57f37f18517f72bc09ee7632")
},
{
"_id" : ObjectId("586e1661d9903c6027a3b47c"),
"RefModel" : "modelB",
"RefId" : ObjectId("57f37f18517f72bc09ee7698")
},
Howto write a populate query for ModelC, to populate with RefId .
it should populate with modelA or modelB which RefModel refers.
I've tried with
ModelC.find({})
.populate({ path: 'RefId', model: 'modelA' })
.populate({ path: 'RefId', model: 'modelB' })
But taking only the last model.
modelC schema.
new mongoose.Schema({
RefModel: String,
RefId:{ type: Schema.ObjectId}});
I could do it with aggregate, but preferring populate.

Fields' names in your database and schema are very confusing, let me explain on more clear example.
Suppose you have 3 models: User, Article and Comment. Article and Comment belongs only to single User. User can have multiple comments and articles (As you shown in your example).
(More efficient and reccommending way). Store comments and articles ids in User model like:
comments: [{ id: '..', ref: Comment }],
articles: [{ id: '..', ref: Article }]
and populate your document:
User.find({})
.populate('comments')
.populate('articles');
Store User id in Comment and Article models like
user: { id: '...', ref: User }
and use mongoose-reverse-populate module for population, for example for comments model:
var reversePopulate = require('mongoose-reverse-populate');
User.find().exec(function(err, users) {
var opts = {
modelArray: users,
storeWhere: "comments",
arrayPop: true,
mongooseModel: Comments,
idField: "user"
}
reversePopulate(opts, function(err, popUsers) {
// populated users will be populated with comments under .comments property
});
});
Also you dont need to keep RefModel in your database, only in schema.

Here, you can try and populate it inside the callback of find itself.
Try this:
ModelC.find({}).exec(function(err,modelC){
//find returns an array of Object, you will have to populate each Object individually.
var modelCObjects =[];
modelC.forEach(function(tempModelC){
//populate via the Model
ModelC.populate(tempModelC,{path : tempModelC.refId , model :tempModelC.refModel},function(err2,newModelC){
//handle newModelC however you want
//Push it into array of modelC Objects
modelCObjects.push(newModelC);
});
//OR
//Populate directly on the resultObject <-im not so sure about this
tempModelC.populate({path : tempModelC.refId , model :tempModelC.refModel},function(err2,newModelC){
//Push it into array of modelC Objects
modelCObjects.push(newModelC);
})
});
//here you have modelCObjects -> Array of populated modelC Objects
});
Please make sure to handle the errors.

Related

how to multi ref in mongoose

I am trying to ref two documents in one property, i have been checking the oficial documentation but i didn't get the solution...
At the moment i am trying this...
items: [{
type: mongoose.Schema.Types.ObjectId,
ref: ['items','users']
}],
In the documentation they mention refPath... but i could not populate both models... any solution for this?
// LINK TO DOCUMENTATION
https://mongoosejs.com/docs/populate.html#dynamic-ref
You don't need to pass refs in arrays. Here is the simple solution:
Mongoose Model (Report.js):
You can clearly see that I did not pass any ref to my Model but still, you can use multiple refs in post/get APIs. I will show you next.
const mongoose = require('mongoose');
const reportSchema = new mongoose.Schema({
reportFrom : {
type: mongoose.Schema.Types.ObjectId,
require: true,
},
reportTo: {
type: mongoose.Schema.Types.ObjectId,
require: true,
},
}
);
module.exports = mongoose.model("report", reportSchema);
Above "reportTo" means the Id of someone post whom the user is going to report or the id of user profile whom the user is going to report. Means "reportTo" may be an ID of User Profile or Post. So, if "reportTo" contains user Id then I have to refer to users collection but if "reportTo" contains post Id then I have to refer to posts collection. So, how I can use two refs. I will simply pass type query from postman to tell which ref to go either posts or users. See below my API request:
APIs file (reports.js)
const reports = req.query.type === "Post" ? await Report.find({reportTo: req.params.id}).populate({
path: 'reportFrom', // attribute name of Model
model: "User", // name of model from where you want to populate
select: "name profilePicture", // get only user name & profilePicture
}).populate({
path: 'reportTo', // attribute name of Model
model: "Post",
}).sort({ _id: -1 })
: req.query.type === "Profile" ? await Report.find({reportTo: req.params.id}).populate({
path: 'reportFrom', // attribute name of Model
model: "User",
select: "name profilePicture",
}).populate({
path: 'reportTo', // attribute name of Model
model: "User",
select: "name profilePicture",
})
.sort({ _id: -1 })
: null
return res.status(200).json(reports);
See the line 7 & 15, you can clearly see how I use two different refs for same attribute. In first case, reportTo is refered to Post Model & in second case reportTo is refered to User Model.

populate or aggregate 2 collection with sorting and pagination

I just use mongoose recently and a bit confused how to sort and paginate it.
let say I make some project like twitter and I had 3 schema. first is user second is post and third is post_detail. user schema contains data that user had, post is more like fb status or twitter tweet that we can reply it, post_detail is like the replies of the post
user
var userSchema = mongoose.Schema({
username: {
type: String
},
full_name: {
type: String
},
age: {
type: Number
}
});
post
var postDetailSchema = mongoose.Schema({
message: {
type: String
},
created_by: {
type: String
}
total_reply: {
type: Number
}
});
post_detail
var postDetailSchema = mongoose.Schema({
post_id: {
type: String
}
message: {
type: String
},
created_by: {
type: String
}
});
the relation is user._id = post.created_by, user._id = post_detail.created_by, post_detail.post_id = post._id
say user A make 1 post and 1000 other users comment on that posts, how can we sort the comment by the username of user? user can change the data(full_name, age in this case) so I cant put the data on the post_detail because the data can change dynamically or I just put it on the post_detail and if user change data I just change the post_detail too? but if I do that I need to change many rows because if the same users comment 100 posts then that data need to be changed too.
the problem is how to sort it, I think if I can sort it I can paginate it too. or in this case I should just use rdbms instead of nosql?
thanks anyway, really appreciate the help and guidance :))
Welcome to MongoDB.
If you want to do it in the way you describe, just don't go for Mongo.
You are designing the schema based on relations and not in documents.
Your design requires to do joins and this does not work well in mongo because there is not an easy/fast way of doing this.
First, I would not create a separate entity for the post details but embedded in the Post document the post details as a list.
Regarding your question:
or I just put it on the post_detail and if user change data I just
change the post_detail too?
Yes, that is what you should do. If you want to be able to sort the documents by the userName you should denormalize it and include in the post_details.
If I had to design the schema, it would be something like this:
{
"message": "blabl",
"authorId" : "userId12",
"total_reply" : 100,
"replies" : [
{
"message" : "okk",
"authorId" : "66234",
"authorName" : "Alberto Rodriguez"
},
{
"message" : "test",
"authorId" : "1231",
"authorName" : "Fina Lopez"
}
]
}
With this schema and using the aggregation framework, you can sort the comments by username.
If you don't like this approach, I rather would go for an RDBMS as you mentioned.

How to update (override) a collection association's value in sails.js?

I have a model that has an attribute that is a collection association:
Take for example, a User model below.
module.exports = {
attributes: {
pets: {
collection: 'pet'
}
}
}
I am aware that I can add pets to a user instance with
user.pets.add(3);
But how could I replace any existing pets with a new group of pets??
Ok I've been playing with the API and found an answer. The following call should update (set) the pets association for a single user. If there were existing pets, this approach would override them.
User.update({id:1}, {pets: [{id: 7}, {id: 8}]}).exec(cb);
You'd remove all the existing pets and create new ones. sails.js has no single special API function to do what you are trying to do, but it's pretty simple either way:
var newPets = [
{ name: 'fluffy', user: 1 },
...
];
Pet.destroy({ user: 1 })
.then(function () {
return _.map(newPets, Pet.create);
})
.then(function (pets) {
// pets are "replaced"
});
Or something like that.

Updating Mongoose Subdocument collection

I have the following mongoose Schema's defined...
var BlogSchema = new Schema({
content: String,
comments:[CommentSchema], //embed the comments
owner: {type: ObjectId , ref: 'User'}
})
var CommentSchema = new Schema({
commentContent : String,
commentPostTime : Number,
likes : [{type: ObjectId , ref: 'User'}],
likeCount: {type: Number, default:0}
})
var UserSchema = new Schema({
})
When a user likes a comment. The user should be added to likes list and the embedded Comment Subdocument in Blog Document should be updated. I am not sure how to achieve this.
Help appreciated.
As your UserSchema doesn't contain much information, it's probably simplest if you don't try to fake a join through Mongoose's population feature. Using MongoDB by itself, accomplishing this would be done with a single operation. Below, I'm just storing the "_id" field of some user document that I want to reference.
> var userID = userdoc._id;
> db.comments.update( { _id : commentID },
{ $push : { likes : userID } } );
Doing this with Mongoose would require you to change your schema slightly:
var CommentSchema = new Schema({
commentContent : String,
commentPostTime : Number,
likes : [ObjectId],
likeCount: {type: Number, default:0}
})
Then, you could just add the new user to the list of likes and increment the likeCount field with the following query:
Comment.findByIdAndUpdate( commentID, { $push : { likes : "samantha" }, $inc : { likeCount : 1 } } );
Is this better than using Mongoose population? Handling your data this way will take an extra step when you look up users from comments. First, you'll have to grab the userID from the list of likes in the comment, then you'll have to run a separate query to get that user's document. However, since your use case doesn't appear to require looking anything up in the user documents, this approach should work well for you. Performance-wise, I believe that this two-step process is what Mongoose is doing under the hood anyway with their population method. Personally, I'm fine with the extra lookup step in exchange for more control over my data.

Searching embedded documents Mongoose + nodejs

Im new to Mongoose, and i'm facing a problem in searching.
These are my Schemas:
var CommentSchema = new Schema({
body : String
, comments : [CommentSchema]
});
var PostSchema = new Schema({
body : String
, comments : [CommentSchema]
});
There is a deep nesting of comments. When somebody answers to the existing comment, how can I find that one?
you can look at the mongoose test suite on github for examples.
model_querying_test
Here is what you are looking for:
test finding based on embedded document fields:
function () {
var db = start(), BlogPostB = db.model('BlogPostB', collection);
BlogPostB.create({comments: [{title: 'i should be queryable'}]}, function (err, created) {
should.strictEqual(err, null);
BlogPostB.findOne({'comments.title': 'i should be queryable'}, function (err, found) {
should.strictEqual(err, null);
found._id.should.eql(created._id);
db.close();
});
});
},
One solution to this is to store Comments as a separate Model which you can query directly, and store references to the related ObjectIds and paths between Comments and Posts.
Using the Populate feature in Mongoose related documents can function similarly to embedded documents, although there are some important differences in the way you query them, and you have to be more careful to keep the relationships populated.
Set it up like this:
var mongoose = require('mongoose')
, Schema = mongoose.Schema
, ObjectId = Schema.Types.ObjectId;
var PostsSchema = new Schema({
body : String,
stories : [{ type: ObjectId, ref: 'Story' }]
});
var CommentsSchema = new Schema({
body : String,
post : { type: ObjectId, ref: 'Post' },
comments : [{ type: ObjectId, ref: 'Comment' }]
});
var Story = mongoose.model('Post', PostsSchema);
var Comment = mongoose.model('Comment', CommentsSchema);
If you do it this way it requires more queries to get the post with all its comments (which will be slower than being able to load the Post and its complete comment hierarchy with a single query) however you'll be able to query comments directly and retrieve the Post they were made on (but not easily find the full path to the comment when its nested).
These are all trade-offs; the best decision (either to recursively search for comments, or store them independently then recursively load them) should be made in the context of your application and its expected usage patterns.
One other caveat; the populate feature is currently limited to a single-level of linked ObjectIds; you have to call it on each comment that is returned to get the full nested dataset. There are several plugins that help with this, such as mongoose-subpopulate, and soon enough it'll be supported natively in Mongoose - see the github issue here.