3

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.

s7vr
  • 73,656
  • 11
  • 106
  • 127
koder613
  • 1,486
  • 5
  • 21
  • 2
    It seems you're treating your Mongo instance as a relational DB, have you considered embedding The Answers in the Question, and the Comments in the Answers (assuming that's where they would go)? – Nimnam1 Oct 19 '20 at 20:45
  • @Nimnam1 I have though about doing that, but I was wondering if the way I have it set up is possible? – koder613 Oct 19 '20 at 21:05
  • Take a look at https://stackoverflow.com/questions/36019713/mongodb-nested-lookup-with-3-levels if that doesn't answer your question I can see if I can come up w/ something. – Nimnam1 Oct 19 '20 at 21:48
  • You are using Mongo in a wrong way, you should denormalize your data. – Mobasher Fasihy Oct 25 '20 at 11:26

2 Answers2

1

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

s7vr
  • 73,656
  • 11
  • 106
  • 127
1

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
})
turivishal
  • 34,368
  • 7
  • 36
  • 59
  • Thank you so much!Could you just explain a bit more what a pipeline is? I am not an expert in mongodb, and I don't really understand what it is? Is it taking results from previous `$lookup` and 'pushing it through a pipe` to use for another operation? – koder613 Oct 20 '20 at 07:08
  • this is a aggregation query and inside we have used separate object with specific operator that is called a pipeline stage, `$lookup` is a pipeline operator to join specific collection and return matching data in array field, there are 2 types of lookup, one is simple lookup that we used in first query, and second is lookup with pipeline, we can use pipeline field and add any pipeline stages inside that pipeline field. – turivishal Oct 20 '20 at 07:18
  • 1
    I think op wants comments from both question and all the answers. You can do it in one query like shown in my answer. – s7vr Oct 20 '20 at 13:23
  • @svr I am not sure, he has not mentioned, i have provided 2 options as per my understanding, he actually looks satisfied in his first comment. I think he is not clear with expectation and also newbie in aggregation query. – turivishal Oct 20 '20 at 13:31
  • @turivishal Would you be able to fetch the comments for the question as well in the second approach? – koder613 Oct 20 '20 at 17:12
  • @koder613 you need just comments only? for the array of comments second answer by ~svr already did you can check for that. or if you want array of objects let me know i will do that. or can you please add expected result in your question. – turivishal Oct 20 '20 at 17:30
  • @turivishal Would you be able to add to the second approach? Please can you fetch the comments for the question matched as well? In your collection design, I think you misunderstood me, a comment can only belong to a question or answer, not both. – koder613 Oct 25 '20 at 22:07
  • @koder613 Can you confirm this [playground](https://mongoplayground.net/p/rRA6BphRCjZ) result, i am not getting you exactly but can you please show your expected result if this will not help you. – turivishal Oct 26 '20 at 05:03