1

I get a document from a mongodb which contains an array with comments for that document. In the comment is the _id of the user which wrote the comment.

I now need to get the username based on the _id of the user, but I'm running into several problems.

I have the following code which, obviously, doesn't work, but I hope it can give you an idea of what I'm trying to accomplish.

//MORE CODE... (No need to show this here, just a promise, some try catch and so on)
let article = await Article.findOne({_id:articleid})   
    for(var i = 0; i<=article.comment.length-1; i++){
        User.findOne({_id:article.comment[i].user}).then((user)=>{
            article.comment[i].username = user.username
        })
    }
return resolve(article)

I looked up several documentations but wasn't able to find a working solution. I tried using Promise.all, played around with a lot of async, await, tried to add a counter into the for-loop and resolve the promise after the loop finished but nothing worked so far.

This is what the article looks like in my db

{
    "_id" : ObjectId("5c18c1cbc47e5e29d42e4b0e"),
    "completed" : false,
    "completedAt" : null,
    "comment" : [ 
        {
            "_id" : ObjectId("5c18c95e328c8319ac07d817"),
            "comment" : "This is a comment",
            "rating" : [ ],
            "user" : ObjectId("5c18b76e73236d2168eda2b4")
        }, 
        {
            "_id" : ObjectId("5c18fb578de5741f20a4e2bd"),
            "comment" : "Another comment",
            "rating" : [ ],
            "user" : ObjectId("5c18b76e73236d2168eda2b4")
        }
    ]
}

I'm rather new to nodejs and mongodb aswell so I hope you can help a newbie like me.

Thank you for your Help

2
  • You can either await in the loop (each findOne) or await a Promise.all over a map instead of a `loop. That said, the actual correct way to do this in Mongo would be to bulk-update and not find and update the documents one-by-one Commented Dec 20, 2018 at 9:47
  • 1
    This is what mongoosejs.com/docs/populate.html is for, or you can attempt to use mongo's aggregation framework if you're feeling suicidal Commented Dec 20, 2018 at 10:05

2 Answers 2

3

There are serveral approaches you can use here based on your convenience

Using async await

let article = await Article.findOne({ _id: articleid }).lean().exec()

await Promise.all(
  article.comment.map(async(obj) => {
    const user = await User.findOne({ _id: obj.user })
    obj.username = user.username
  })
)

console.log(article)

Using $lookup aggregation 3.6

Since mongodb has its own powerfull $lookup aggregation operator to join multiple collection and probably the better approach without any iteration

Article.aggregate([
  { "$match": { "_id": mongoose.Types.ObjectId(articleid) }},
  { "$unwind": "$comment" },
  { "$lookup": {
    "from": "users",
    "let": { "userId": "$comment.user" },
    "pipeline": [
      { "$match": { "$expr": { "$eq": ["$$userId", "$_id"] }}}
    ],
    "as": "comment.user"
  }},
  { "$unwind": "$comment.user" },
  { "$group": {
    "_id": "$_id",
    "comment": { "$push": "$comment" },
    "completed": { "$first": "$completed" },
    "completedAt": { "$first": "$completedAt" }
  }}
])

Using $lookup aggregation 3.4

Article.aggregate([
  { "$match": { "_id": mongoose.Types.ObjectId(articleid) }},
  { "$unwind": "$comment" },
  { "$lookup": {
    "from": "users",
    "localField": "comment.user",
    "foreignField": "_id",
    "as": "comment.user"
  }}
  { "$unwind": "$comment.user" },
  { "$group": {
    "_id": "$_id",
    "comment": { "$push": "$comment" },
    "completed": { "$first": "$completed" },
    "completedAt": { "$first": "$completedAt" }
  }}
])
Sign up to request clarification or add additional context in comments.

1 Comment

Both those approaches worked like a charm. Thank you very much for you quick and efficient help :)
-2

You can try like the following way

const d = {
    "_id" : ObjectId("5c18c1cbc47e5e29d42e4b0e"),
    "completed" : false,
    "completedAt" : null,
    "comment" : [ 
        {
            "_id" : ObjectId("5c18c95e328c8319ac07d817"),
            "comment" : "This is a comment",
            "rating" : [ ],
            "user" : ObjectId("5c18b76e73236d2168eda2b4")
        }, 
        {
            "_id" : ObjectId("5c18fb578de5741f20a4e2bd"),
            "comment" : "Another comment",
            "rating" : [ ],
            "user" : ObjectId("5c18b76e73236d2168eda2b4")
        }
    ]
}

d.comment.forEach( async (obj, index) => {
    await new Promise((res) => {
            obj.counter = index;
            res();
    })
});

console.log(d);

For reference please take a look on following link Asycn/Await using forEach

5 Comments

So in case of you have a list of 100 comments, it will take 100 seconds? You should avoid doing this.
``` setTimeout(() => { obj.counter = index; res(); }, 1000);`` the piece of code is for reference purpose not the actual one. I just gave an idea how can we do this. I didn't add the full-fledged code to copy paste. I think it's quite obvious to avoid timeout inside a loop :)
It makes sense. But it is still misleading.
Why to use new Promise as async function itself returns promise.
yes async default returns promises, but before updating the above answer used with setTimout fn which by default doesn't return promises as @crellee feels that above example with setTimeout make it confusing

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.