Populate child collection - mongodb

This is example from mongoose docs about populate:
const personSchema = Schema({
_id: Schema.Types.ObjectId,
name: String,
age: Number,
stories: [{ type: Schema.Types.ObjectId, ref: 'Story' }]
});
const storySchema = Schema({
author: { type: Schema.Types.ObjectId, ref: 'Person' },
title: String,
});
So, person has list of stories and when we fetch persons we can include stories by using populate('stories'). So far so good.
But in order for that to work, when creating Story, we need to add storyId to stories list in Person. I am coming from SQL background, where that does not need to be done, it would find related stories automatically based on authorId on Story.
So the question is, can it be done in same way here, without need to update stories property on Person?

I found a solution, its called virtual property:
AuthorSchema.virtual('posts', {
ref: 'BlogPost',
localField: '_id',
foreignField: 'author'
});
This way I can populate posts in query without saving postIds in Author/Person schema.

Related

How to use populate, within select, in MongoDB?

Suppose I have two schemas on MongoDB:
const personSchema = Schema({
_id: Schema.Types.ObjectId,
name: String,
email: String,
things: [{ type: Schema.Types.ObjectId, ref: 'Thing' }]
});
const thingSchema = Schema({
_id: Schema.Types.ObjectId,
title: String,
fans: [{ type: Schema.Types.ObjectId, ref: 'Person' }]
});
Every time a user logs in, I would like to show the things that they have posted, as well as the fans that are following each of the things. I am able to use populate and select to get to that:
const user = await personModel
.findOne({ _id: req.user._id })
.populate({
path: "things",
select: ["title", "fans"]
}),
However, I am only getting the id of each fan, and not the fan's name and email. I can't quite figure out how to use populate to reference the person collection again.
The outcome I am trying to achieve is that:
the user object would have an array of things
the thing object would have an array of fans
the fan object would have two values - name and email of the fan
You can do nested population with:
const user = await personModel
.findOne({ _id: req.user._id })
.populate({
path: 'things',
select: ['title', 'fans'],
populate: { path: 'fans' },
})
.exec();

Getting total number of likes that user received by going through all his/her posts MongoDB

I'm currently using MERN stack to create a simple SNS application.
However I got stuck trying to come up with a query that could go through all of the post that user posted and get the sum of likes.
Currently I've created 3 Schemas. User, Post, Reply.
User
const userSchema = new Schema({
facebookId: {
required: true,
type: String,
},
username: {
required: true,
type: String,
},
joined: Date
})
POST
const postSchema = new Schema({
title: String,
body: String,
author: { type: Schema.Types.ObjectId, ref: "User" },
datePosted: Date,
reply: [{ type: Schema.Types.ObjectId, ref: 'Reply'}],
tag: [ {type: String} ]
});
REPLY
const replySchema = new Schema({
title: String,
body: String,
author: { type: Schema.Types.ObjectId, ref: "User" },
datePosted: Date,
post: [{ type: Schema.Types.ObjectId, ref: 'Post'}],
likes: [{ type: Schema.Types.ObjectId, ref: "User" }] // storing likes as array
});
As you can see I have added likes field in Reply Schema as an array that takes in User ObjectId.
Say some user has posted 4 replies and each of them received 1 ,3, 4, 5 likes respectively. In the leaderboard section I want to display user info with the total number of counts that they received from all of their replies which is 1+3+4+5 = 13 likes.
Can this be achieved without adding additional fields or is there a better way of structuring the schema.
If this field is going to be shown publicly then I personally recommend that instead of calculating on the fly you pre-calculate it and save it on the user as aggregating data is expensive and should not be a part of your app's logic, especially if this needs to be calculated for each user for the leaderboard feature.
With this said here is how you can calculate it with the aggregation framework
db.replies.aggregate([
{
$match: {
author: userid
}
},
{
$group: {
_id: null,
likes: {$sum: {$size: '$likes'}}
}
}
]);
As I said I recommend you do this offline, run this once for each user and save a likeCount field on the user, you can then update your other routes where a like is created to update the user like count using $inc.
// .. new like created ...
db.users.updateOne({_id: liked_reply.author}, {$inc: {likeCount: 1}})
And now finding the leaderboard is super easy:
const leaders = await db.users.find({}).sort({likeCount: -1}).limit(10) //top 10?
you can use the models aggregate function to do that:
const userid = 1234
post.aggregate([
{ $match: { _id: userid } },
{ $lookup: {
from: 'Reply',
localField: 'reply',
foreignField: '_id',
as: 'replies'
} },
{ $group: {
_id: false,
sumoflikes: { $sum: '$replies.likes' }
} }
])
the structure works as follows:
get all the posts from a user with 'userid'
join the table with 'Reply'
sum all of the reply.likes
(it could be that you need to throw in a $unwind: '$replies between 2 and 3 there, i am not 100% certain)

How to structure a referenced document that needs to be filtered on mongoDB?

I have an user document and a transactions document.
The Transaction has a owner (User),
The User has an set of Transactions that has no limit to grow.
I need to get the User transactions and filter it by some properties like, date range and paid or non paid.
As my transactions Schema is unbounded I used the mongoose virtuals to populate the User with the transactions, but as I see it's not possible to make MongoDb queries to a virtual because it's not really in the Data Base.
The use Schema:
const userSchema = new mongoose.Schema(
{
name: {
type: String,
required: true,
},
email: {
type: String,
required: true,
trim: true,
unique: true,
},
...
},
);
userSchema.virtual('transactions', {
ref: 'Transaction',
localField: '_id',
foreignField: 'owner',
});
The Transaction Schema:
const transactionSchema = new Schema({
amount: {
type: Number,
required: true,
},
...
owner: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true,
},
});
I feel like I made some mistake on modeling the database this way. Which would be the best approach to this case?
I don't think it's a good idea to use virtuals here. You have the user reference in your transaction model and it's enough. You can query whatever you like with mongo aggregations
You can do the aggregation on transactions collection and use $lookup (as described here) to populate the users or query them.

Is there anyway to remove one Document and then update ref model's ObjectId array automatically?

Here my models.
const UserSchema = mongoose.Schema({
name: String,
comments: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'Comment'
}
]
});
const CommentSchema = new mongoose.Schema({
comment: String,
creatorId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}
});
User could have many comments.
So User and Comment is one-to-many relationship.
When User create a new comment, A new Comment is created and it's ObjectId is pushed to User's comments array.
What I want to do is,
delete Comment's ObjectId automatically when delete one Comment.
I know I can do this with two queries.
Is there anyway to combine these two to one?
await user.comments.remove(comment.id);
await comment.remove();
You can't run remove on two collections / schemas however you can take advantage of Post Middleware and define such middleware on CommentsSchema:
CommentsSchema.post('remove', function(comment) {
await UserSchema.update({ _id: comment.creatorId }, { $pull: { comments: comment._id } })
});

Retrieve relationship from a child instance on MongoDB

In MongoDB, I have a One-To_many reference relationship.
A has many B.
A has a property called B_ids, so I can retrieve all the B instances owned by a a particular A instance.
My question is: looking to an instance of B, how can I retrieve the A instance that owns it?
Thanks!
In order to do that you can try this:
var personSchema = Schema({
_id: Schema.Types.ObjectId,
name: String,
age: Number,
stories: [{ type: Schema.Types.ObjectId, ref: 'Story' }]
});
var storySchema = Schema({
author: { type: Schema.Types.ObjectId, ref: 'Person' },
title: String,
fans: [{ type: Schema.Types.ObjectId, ref: 'Person' }]
});
in this way you can retrieve A from B using populate.
Story.find().populate('author')
Example borrowed from mongoose populate website.