single chained query mongodb / mongoose to fetch all comments - javascript

I am trying to develop a personal project, a website that functions in a similair way to Stack Exchange, a user can ask a question which can receive multiple answers. Each question and answer can have multiple comments.
I am using nodeJS for my backend.
How can I fetch all comments for all the answers on a particular question in a single mongoDB / mongoose query?
It would be even more helpful if you could tell me how to fetch all comments for all the answers on a particular question as well as all the comments for the question in a single mongoDB / mongoose query?
Mongoose Schemas:
const questionSchema = new mongoose.Schema({
title: String,
content: String
})
const answerSchema = new mongoose.Schema({
questionId: String,
content: String,
})
const commentSchema = new mongoose.Schema({
idQuestion: String, // nullable
idAnswer: String, // nullable
content: String
})
Currently, I am performing a mongoose query to find all the answers for a particular questions. Then, using forEach, performing a mongoose query on each answer to find all the comments for each answer. I believe this is very taxing, performance wise and is not an ideal way to do what I would like to achieve.

You can try below aggregation. Match on question id followed by join to lookup all the answers ids with question id followed by lookup to pull in all comments.
db.questions.aggregate([
{"$match":{"_id":input_question_id}},
{"$lookup":{
"from":"answers",
"localField":"_id",
"foreignField":"questionId",
"as":"answers"
}},
{"$lookup":{
"from":"comments",
"let":{"ids":{"answers_id":"$answers._id","question_id":"$_id"}},
"pipeline":[
{"$match":{"$expr":{
"$or":[
{"$eq":["$idQuestion","$$ids.question_id"]},
{"$in":["$idAnswer","$$ids.answers_id"]}
]
}}}
],
"as":"comments"
}},
{"$project":{"comments":"$comments.content"}}
])
Working example here - https://mongoplayground.net/p/qBlKqk-JsxA

You can try,
$match your conditions questionId
$lookup join with comments
db.answers.aggregate([
{ $match: { questionId: 1 } },
{
$lookup: {
from: "comments",
localField: "_id",
foreignField: "idAnswer",
as: "comments"
}
}
])
Playground
Second approach, if you want to select questions with all answers and comment then try,
$match your conditions
$lookup with pipeline join with answers collection
pipeline field can allow to add all stages of pipeline that we are using in root level
$match questionId and get answers
$lookup join with comments collection
db.questions.aggregate([
{ $match: { _id: 1 } }, // this is optional if you want to select all questions then remove this
{
$lookup: {
from: "answers",
let: { questionId: "$_id" },
pipeline: [
{ $match: { $expr: { $eq: ["$$questionId", "$questionId"] } } },
{
$lookup: {
from: "comments",
localField: "_id",
foreignField: "idAnswer",
as: "comments"
}
}
],
as: "answers"
}
}
])
Playground
Show or hide extra fields, you can use $project operator at the end of above query,
You can show fields as per your needs
{
$project: {
_id: 1,
content: 1,
"comments._id": 1,
"comments.content": 1
}
}
Playground
Suggestions:
I am not sure, you have already done or not, but try to define object id type in reference field instead of string type, like I have updated your schema, this will add a default index in object id and this will increase a speed of fetching data,
const answerSchema = new mongoose.Schema({
questionId: mongoose.Types.ObjectId,
content: String,
})
const commentSchema = new mongoose.Schema({
idQuestion: mongoose.Types.ObjectId, // nullable
idAnswer: mongoose.Types.ObjectId, // nullable
content: String
})

Related

how to take result of aggregation/JSON to update a collection document?

I am making a polling website. Currently I have two collections Polls and Votes. I am using the aggregation pipeline to get the number of votes for each movie. I am having difficulty wrapping my head around updating the poll based on the vote collection. This is the vote Schema:
poll: objectId
votedMovies: Array
0: Object
id: ObjectId
title: string
This is my poll Schema:
_id: ObjectID
pollType: String
totalVotes: Number
movies: Array
0: Object
id: ObjectID
title: String
votes: Number
So far I have an aggregation pipeline that does the following:
let voteCollection = await db.collection('votes').aggregate([
{
$match: {poll: id}
},
{
$unwind: "$votedMovies"
},
{
$group: {_id: "$votedMovies.id", totalVotes: {$sum: 1}}
}
]).toArray()
That spits out something like this:
[{"_id":10674,"totalVotes":2},
{"_id":99861,"totalVotes":1},
{"_id":299534,"totalVotes":4},
{"_id":637157,"totalVotes":3},
{"_id":24428,"totalVotes":5}]
How do I update the poll document so that it has the current number of votes? Am I on the right track with the aggregation pipeline?
You should be able to update each movie votes with:
for (const vote of voteCollection) {
await db.collection('polls').updateOne(
{
_id: id, // pool id
'movies._id': vote._id,
},
{
'movies.$.votes': vote.totalVotes,
}
);
}

Mongodb multiple $lookup depends on the first lookup

Hi i have a mongo scheme called "payments" that has a 2 keys that optional:
userId or representativeId (if userId exist representativeId not exist and same about representativeId).
When i found the payments scheme based on cheque scheme that using the $match to filter the result based on my data, I bring all payments in the first lookup that match for my query , in the second and third lookup I want to bring the user data / representative data.
Maybe it will be mix of them its ok, its what I want to see if one of them does not exist the other must to be exist.
I want to get the final array that include the user or the representative or mix of them in the same array.
I am using aggregate to implement this.
the problem its give me back a empty array when 2 of the lookup show(user and representative),
but when I comment the lookup of the user or the representative, and i left with 2 lookup one for payment and after that last lookup user / representative and its work me like I want but just for user / representative just if i remove one of the lookup.
I want its bring my array with two of them.
const userAndRepData = await ChequeDB.aggregate<{[key: string]: any}>([
{
$match: {
$and: [
{
'chequeNumber': {
$in: chequeData.map(c => c.chequeNumber)
},
'accountNumber': {
$in: chequeData.map(c => c.accountNumber)
}
}
]
}
},
{
$lookup: {
from: 'payments',
localField: 'paymentId',
foreignField: '_id',
as: 'payment'
}
},
{
$unwind:'$payment'
},
{
$lookup: {
from: 'representatives',
localField: 'payment.representativeId',
foreignField: '_id',
as: 'representative'
}
},
{
$unwind: '$representative'
},
{
$lookup: {
from: 'users',
localField: 'payment.userId',
foreignField: '_id',
as: 'user'
}
},
{
$unwind: '$user'
},
{
$project: {
user: 1,
_id: 0,
representative: 1
}
}
]);
Unfortunately, you can't use data from one $lookup in the next one since the data from $lookup still hasn't been loaded at the moment query is executing, i.e. payment.representativeId will be null.
You can write resource intensive queries that would work around this, but the easiest and best (performance-wise) way to execute this query would be to have representativeId and userId stored on the Cheque collection.

how do I get and display the referenced data in mongodb?

I have created two model users and post where users reference post.
This is my user model.
var mongoose = require("mongoose");
mongoose.connect("mongodb://localhost:27017/demo",{ useNewUrlParser: true });
var Post = require('./post');
var UserSchema = mongoose.Schema({
username: String,
password: String,
posts:[{
type:mongoose.Schema.Types.ObjectId,
ref: Post
}] });
This is my post model
var postSchema = mongoose.Schema({
title:String,
content: String,
likes:Number
});
When I create and save a new post to a specific user I get an id in the post field of the user model. like this:-
{
posts: [
60e33a2d18afb82f8000d8f0,
],
_id: 60d9e931b5268920245c27f0,
username: 'user1',
password: '1234',
__v: 5
}
How do I access and display the contents of the post field?
Thanks!
You can get and display Reference data in MongoDB with Aggregate.
Like:
await User.aggregate([
{
$match: { _id: ObjectId(user._id)}
},
{
$lookup: { from: "Posts", localField: "post", foreignField: "_id", as: "posts" }
},
{
$unwind: { path: "$users", preserveNullAndEmptyArrays: true,}
}, {
$project: {
" Now you can select here whatever data you want to show "
}
}
])
PS: If you have any confusion please do comment.
Thanks
Mongoose has a powerful alternative to using aggregate. You can do this with populate .
Use it like this 👇 this will populate the ids inside the posts field with the actual post documents.
User.findById(id).populate('posts')
// Full example
try {
const user = await User.findById(id).populate('posts').exec();
for (let post of user.posts) {
console.log(post.content);
}
} catch (ex) {
console.error(ex);
}
If I may, a small suggestion on how to setup your document model or just to show you an alternative. Don't reference the Posts inside your User Document. Do it the other way round and reference the User inside your Post Document.
const postSchema = mongoose.Schema({
title : String,
content : String,
likes : Number,
user : { type:mongoose.Schema.Types.ObjectId, ref: 'User' }
});
// GET ALL POSTS FROM ONE USER
Post.find({user:id});
This way you don't need to use populate when trying to show all posts of 1 user.

Mongoose virtuals are not added to the result

I'm building a quiz editor where rounds contain questions and questions can be in multiple rounds. Therefor I have the following Schemes:
var roundSchema = Schema({
name: String
});
var questionSchema = Schema({
question: String,
parentRounds: [{
roundId: { type: Schema.Types.ObjectId, ref: 'Round'},
isOwner: Boolean
}]
});
What I want is to query a round, but also list all questions related to that round.
Therefor I created the following virtual on roundSchema:
roundSchema.virtual('questions', {
ref : 'Question',
localField : '_id',
foreignField : 'parentRounds.roundId'
});
Further instantiating the Round and Question model and querying a Round results in an object without questions:
var Round = mongoose.model('Round', roundSchema, 'rounds');
var Question = mongoose.model('Question', questionSchema, 'questions');
Round.findById('5ba117e887f66908ae87aa56').populate('questions').exec((err, rounds) => {
if(err) return console.log(err);
console.log(rounds);
process.exit();
});
Result:
Mongoose: rounds.findOne({ _id: ObjectId("5ba117e887f66908ae87aa56") }, { projection: {} })
Mongoose: questions.find({ 'parentRounds.roundId': { '$in': [ ObjectId("5ba117e887f66908ae87aa56") ] } }, { projection: {} })
{ _id: 5ba117e887f66908ae87aa56, __v: 0, name: 'Test Roundname' }
As you can see, I have debugging turned on, which shows me the mongo queries. It seems like the second one is the one used to fill up the virtual field.
Executing the same query using Mongohub DOES result in a question:
So why doesn't Mongoose show that questions array I'm expecting?
I've also tried the same example with just one parentRound and no sub-objects, but that also doesn't work.
Found the answer myself...
Apparently, I have to use
console.log(rounds.toJSON({virtuals: true}));
instead of
console.log(rounds);
Why would Mongoose do such a devil thing? :(

Querying a collection from ObjectId's with $ne and $or condition

I need to serve an unanswered (by user) question to the user where user model holds the questions asked in past with an array, say it user.asked.
I want to clarify that I promisified mongoose library, hence I'm using those functions, nothing fancy.
User model:
var UserSchema = new Schema({
:
asked: [{ type: Schema.Types.ObjectId, ref: 'Question' }]
})
Here's what I tried so far:
Question.findAsync({
_id: {
$ne: {
$or: req.user.asked
}
}
})
which results: Cast to ObjectId failed for value \"[object Object]\" at path \"_id\".
I also tried to make an aggregation (example given):
{
$match: {
_id: {
$ne: {
$or: [ ObjectId("56ccfb048f896e0c2d06d08f"), ObjectId("56ccfb048f896e0c2d06d98f") ]
}
}
}
}
however this returns all of the documents in the collection (even the referred ones).
Do you have any suggestions?
You can use the $nin operator to find a doc with a field that matches none of the values in an array:
Question.findAsync({
_id: {
$nin: [ObjectId("56ccfb048f896e0c2d06d08f"), ObjectId("56ccfb048f896e0c2d06d98f")]
}
})

Categories

Resources