Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

Jason McIver


A Javascript developer excited about Meteor, Mongo, React and Node.

Mongo query deep array

Being a document database Mongo is great at storing your data in a pre-joined fashion. To complement this Mongo provides an interesting way of querying this data structure.

Here is an example structure used for a small Flashcard memory app. It provides decks with many cards - each card with many single popquizzes the user can perform at anytime.
Quizzes are similar to popquizes but force the user to proceed through the whole deck.

Flashcards data structure

decks:[{  
    _id: ObjectId(),
    name: String,
    cards:[{
        _id: ObjectId(),
        front:String,
        back:String,
        popquizzes:[{
            promptDate: Date,
            correct: Boolean
        }]
    }],
    quizzes: [{
        _id: ObjectId(),
        quizDate: Date,
        completed:Boolean,
        results:[{
            card_id:String,
            skipped:Boolean,
            correct:Boolean
        }]
    }]
}]

Queries

Mongo lets us perform queries we need:

Get a single card

Mongo lets us query directly on any cards without knowing which it deck it belongs to.

db.flashcards.find(  
  {'decks.cards._id': 3},
  {'decks.cards.$':1}
)

Remeber when querying on subdocuments it is beneficial to create indexes. Chances are you will have many more subdocs than top level docs (1:n)

In this query I have made use of the second find() parameter projection returning only what you specify. The dollar $ sign operator tells Mongo to return the first subdoc that was found. This is great for sliming down your data to optimise performance and security.

Update a single subdoc

Another thing about the $ operator is it can be used for updating subdocs:

db.flashcards.update(  
  {'decks.cards._id': 3},
  {$set:{'decks.cards.$.back':'42'}}
)

Always be careful doing an update. Without $set you will completely replace the whole document!

Only the single subdoc that was found has been updated with the field back to '42'. This is great for targeting subdocs and fields, not lets try it on a deeper level.

Update a single subsubdoc

What if for some reason we needed to update a single popquiz. Without a popquiz id we may think we can use $ a second time.

db.flashcards.update(  
  {'decks.cards._id': 3, 'decks.cards.popquizzes.promptDate': Date('some matching date')},
  {$set:{'decks.cards.$.popquizzes.$.correct':false}}
)

Unfortunety we cannot use $ a second time as Mongo only allows us to use it once. The solution is to use the array index:

The optimal solution would be to just give an id field to each popquiz and query it directly. Sometimes due to decisions outside our control this maynot be an option.
We certainly do not want to replace the whole popquzzies array for this card as we may not have the entire array set. A cleaner approach would be to use the array index if we knew it:

db.flashcards.update(  
  {'decks.cards._id': 3},
  {$set:{'decks.cards.$.popquizzes.0.correct':false}}
)

Get the popquiz array

db.flashcards.find(  
  {'decks.cards._id': 3},
  {'decks.cards.$.popquizzes':1}
)
// Get the index using Underscore or Lodash findIndex()
var index = _.findIndex(popquizzes, {promptDate: Date('some matching date')})

// Now update the popquiz
// prepare the query with the index
var setData = Object;  
var location = 'decks.cards.$.popquizzes.'+index+'.correct';  
setData[location] = false;  
// perform update
db.flashcards.update(  
  {'decks.cards._id': 3},
  {$set:setData}
)