3

OK I have been trying to tackle this for a while now but seem to be running in circles.

My main goal is to dive into the 2nd nested array and remove the 'isCorrectAnswer' property completely... The object is basically an item, which has 2 questions, which each have 3 answer options...

During the 'answering' timeframe of the app, I need to remove the 'isCorrectAnswer' so that no users can cheat by looking into the data being passed... Then once the 'answering' timeframe has passed, this will be returned as the complete object to indicate to the user which answers were the correct ones.

An example of the 'item' object with it's questions and answers being returned from MongoDB is:

{
    "_id" : ObjectId("5397e4b75c4c9bf0509709ab"),
    "name" : "Item Name",
    "description" : "Item Description",
    "questions" : [
        {
            "_id" : ObjectId("5397eb925d2664177b0fc5a5"),
            "question" : "Item Question 1",
            "answers" : [
                    {
                        "_id" : ObjectId("5397eb925d2664177b0fc5a6"),
                        "answer" : "Item Question 1 - Answer 1",
                        "isCorrectAnswer" : true
                    },
                    {
                        "_id" : ObjectId("5397eb925d2664177b0fc5a7"),
                        "answer" : "Item Question 1 - Answer 2",
                        "isCorrectAnswer" : false
                    },
                    {
                        "_id" : ObjectId("5397eb925d2664177b0fc5a8"),
                        "answer" : "Item Question 1 - Answer 3",
                        "isCorrectAnswer" : false
                    }
                ]
        },
        {
            "_id" : ObjectId("5397eb925d2664177b0fc5a9"),
            "question" : "Item Question 2",
            "answers" : [
                {
                    "_id" : ObjectId("5397eb925d2664177b0fc5aa"),
                    "answer" : "Item Question 2 - Answer 1",
                    "isCorrectAnswer" : false
                },
                {
                    "_id" : ObjectId("5397eb925d2664177b0fc5ab")
                    "answer" : "Item Question 2 - Answer 2",
                    "isCorrectAnswer" : true
                },
                {
                    "_id" : ObjectId("5397eb925d2664177b0fc5ac"),
                    "answer" : "Item Question 3 - Answer 3",
                    "isCorrectAnswer" : false
                }
            ]
        }
    ]
}

Now, based on what I have learned from the MongoDB course...

My first goal was to do a double unwind to flatten everything out into single object structures.

So first to steps in the aggregation pipeline are:

{ "$unwind": "$questions" },
{ "$unwind": "$questions.answers" }

This works fine...

My next step was to run $project to remove the 'isCorrectAnswer' property:

   { "$project": {
       "_id":1,
       "name":1,
       "description":1,
       "questions":{
           "_id":"$questions._id",
           "question":"$questions.question",
           "answers":{
               "_id":"$questions.answers._id",
               "answer":"$questions.answers.answer"
           }
       }
   }}

This works fine as well...

Now where I am falling short is combining the object back together again into the ORIGINAL STRUCTURE (without the 'isCorrectAnswer' property)...

I can run this $group command next in the pipeline, which DOES work but the answers are not grouped back in with their question

   { "$group":{
       "_id":{
           "_id":"$_id",
           "ordinal":"$ordinal",
           "name":"$name",
           "description":"$description",
           "benefits":"$benefits",
           "specialOffer":"$specialOffer",
           "choicePoints":"$choicePoints",
           "bonusPoints":"$bonusPoints",
           "redemptionPoints":"$redemptionPoints",
           "questions":"$questions"
       }
   }}

I am still grasping the aggregation framework and more to do with the $group command... I am wondering if there are any steps I should be doing differently or how to run a 2nd $group to combine the 'answers' together.

I also assume I will need to run a final $project to clean up the '_id' properties that are getting added via the $group

Thanks for the help.

Derek

Neil Lunn
  • 148,042
  • 36
  • 346
  • 317

2 Answers2

3

Since your requirement is to just "project" the document so the field is masked, yes the aggregation framework is a tool for doing this. It takes a bit to get your head around the process when unwinding arrays and reconstructing though.

So what you wanted was this:

db.collection.aggregate([
    { "$unwind": "$questions" },
    { "$unwind": "$questions.answers" },
    { "$group": { 
        "_id": {
            "_id": "$_id",
            "name": "$name",
            "description": "$description",
            "qid": "$questions._id",
            "question": "$questions.question"
        },
        "answers": {
            "$push": {
                "_id": "$questions.answers._id",
                "answer": "$questions.answers.answer"
            }
        }
    }},
    { "$project": {
        "questions": {
            "_id": "$_id.qid",
            "question": "$_id.question",
            "answers": "$answers"
        }
    }},
    { "$sort": { "_id": 1, "questions._id": 1 } },
    { "$group": {
        "_id": "$_id._id",
        "name": { "$first": "$_id.name" },
        "description": { "$first": "$_id.description" },
        "questions": { "$push": "$questions" }
    }}
])

But really, if you have a MongoDB 2.6 or greater version then you do not need to $unwind and $group the results back together in order to omit that field. You can now just do this using $project and the $map operator which works with arrays:

db.collection.aggregate([
    { "$project": {
        "name": 1,
        "description": 1,
        "questions": {
            "$map": {
                "input": "$questions",
                "as": "q",
                "in": {
                    "$ifNull": [
                        { 
                            "_id": "$$q._id",
                            "question": "$$q.question",
                            "answers": {
                                "$map": {
                                    "input": "$$q.answers",
                                    "as": "el",
                                    "in": {
                                        "$ifNull": [
                                            { "_id": "$$el._id", "answer": "$$el.answer" },
                                            false
                                        ]
                                    }
                                }
                            }
                        },
                        false
                    ]
                }
            }
        }
    }}
])

Sorry for the indentation scrolling off the page a little there, but it is still easier to read by comparison.

The first $map processes the questions array in place and feeds to an inner $map that returns the inner answers array documents without the "isCorrectAnswer" field. It uses it's own variables to represent the elements, and the usage of $ifNull in there is just because the "in" part of the $map operator expects to evaluate a condition on each of those elements.

Overall a bit faster, as you do not have to go through the $unwind and $group operations just to remove the field. So it really becomes just the "projection" that you might expect.

Neil Lunn
  • 148,042
  • 36
  • 346
  • 317
1

You can use the $unset operator, combined with the dot notation, to remove the properties.

For example, the following command will remove the isCorrectAnswer property from the first answers array:

db.collectionname.update({_id:"SOME_ID"},{$unset:{"questions.0.answers.0.isCorrectAnswer":""}})

Punnoose
  • 72
  • 3
  • Actually the question was about "projection" and document re-shaping with the aggregation framework rather than altering the actual documents in the collection. I will admit the original title was misleading. – Neil Lunn Jun 11 '14 at 06:55
  • Yes I guess I messed up the wording a bit in the title. Thank you for the input! – DerekRosien Jun 17 '14 at 13:56