The Struggles of a MongoDB Newbie

They say “don’t knock it ‘til you try it.” MongoDB is no exception to this rule. So I decided to try it, and I’d like to briefly share my first impression.

For a database working to eliminate the need for relations, why does the functionality for subdocuments seem so limited? I'm ready to embrace JSON as a data model, but use cases such as nested arrays need to be considered when designing queries. I’ll summarize my struggle with a single, troubling use case I encountered.

The Phonebook Example

Consider the following JSON object, a Phonebook named Friends containing a list of phone numbers:

{
    "_id": "QLW5MmZL4zG3g9Zik",
    "name": "Friends",
    "selected": false,
    "numbers": [
        {
            "number": "+15555555555",
            "selected": false
        },
        {
            "number": "+15555555555",
            "selected": false
        }
    ]
}

Say that I when I change the document's selected value to true, I want every item in the numbers array to also be set to true, and vice versa. The first part of this query is easy:

Phonebooks.update(
    { _id: "QLW5MmZL4zG3g9Zik"},
    { $set: { selected: true}}
);

After looking through MongoDB’s docs for information on updating nested arrays I came across the positional $ operator. It looked pretty straight forward.

The Logical Solution

To solve my problem, I tried to make a seemingly logical update query similar to the one above:

Phonebooks.update(
    { _id: "QLW5MmZL4zG3g9Zik"}, 
    { $set: {"numbers.$.selected": true }}
);

However, my query resulted in the following message:

MongoError: The positional operator did not find the match needed from the query. Unexpanded update: numbers.$.selected

Hmm, that’s strange. Maybe the syntax is wrong? As I read further into the documentation I see notes like this:

“The $ operator can update the first array element that matches multiple query criteria specified with the$elemMatch() operator.”

“When used with update operations, e.g. db.collection.update() and db.collection.findAndModify(), the positional $ operator acts as a placeholder for the first element that matches the query document.”

“The positional $ operator cannot be used for queries which traverse more than one array… because the replacement for the $ placeholder is a single value”

It appears $ can traverse entire arrays, but it can only ever return one element. This eliminates a lot of the usefulness of such an operator. Nowhere in the documentation does it mention why this operator only returns one element. It also fails to mention alternative approaches to the problem, which I assume to mean that there are none (beside refactoring the entire database, of course).

After searching Google and Stack Overflow for about an hour, I ended up here. This JIRA ticket confirmed that updating all items in a document’s array is not possible via a single update query. The most frustrating part of all this is the fact that this ticket has accumulated hundreds of votes over the five years it has been open.

My Solution

The solution to my problem ended up as such:

var numbers = Phonebooks.find(
    { numbers: { $elemMatch: {"selected": false}}}
);

numbers.forEach(function (setter) {
    for (var index in setter.numbers) {
        Phonebooks.update(
            { _id: "QLW5MmZL4zG3g9Zik",
             "numbers.number": setter.numbers[index].number 
            }, 
            { $set: {"numbers.$.selected": true} }
        );
    }
});

The code queried for every number in the document set to false, iterated over the cursor that was returned, and made another query to update every single element in that cursor. As a result, my solution was much slower, but I avoided the need to refactor my entire database.

Concluding Thoughts

I’m not a full-stack developer and I haven’t designed many apps that require persistent data. It’s tough to want to use MongoDB again after writing such ugly code. I sincerely hope the MongoDB team will be addressing this feature request soon.

If you have any feedback on this post feel free to reach to me on Twitter @brodan_.