OdeToCode IC Logo

Using elemMatch in a MongoDB Query

Monday, May 27, 2013

I introduced a latent bug in some code recently while trying to find documents with a specific set of values embedded inside an array. For example, consider the following documents:

{
    "name" : "Thing1",
    "children" : [  
        { "name" : "C1", "value" : "A" },
        { "name" : "C2", "value" : "B" },
        { "name" : "C3", "value" : "C" }]
}, {
    "name" : "Thing2",
    "children" : [
        { "name" : "C3", "value" : "A" },
        { "name" : "C4", "value" : "B" },
        { "name" : "C5", "value" : "C" }]
}, {
    "name": "Thing3",
    "children": [
        { "name": "C5", "value": "A" },
        { "name": "C6", "value": "B" },
        { "name": "C7", "value": "C" }]
}

Let’s say we only want documents with a child entry that has a name of C3 AND a value of C (only Thing1 should match this criteria because of the last child in its array). It’s tempting to write a query with an AND clause and feel good about the C# code.

var query = things.Find(
    Query.And(
        Query.EQ("children.name", "C3"),
        Query.EQ("children.value", "C")
    ));

But MongoDB, being document oriented, interprets the query as “give me all the documents that have any child with a name of C3, and any child with a value of C. In other words, the name and value combination don’t need to match inside the same array element. A quick test from the shell can verify the behavior.

> db.things.find({"children.name":"C3", "children.value":"C"},{name:1, _id:0})
{ "Name" : "Thing1" }
{ "Name" : "Thing2" }

The $elemMatch query operator tells Mongo to only match documents when at least one element in the array satisfies the entire criteria.

var query = things.Find(
    Query.ElemMatch("children",
        Query.And(
            Query.EQ("name", "C3"), 
            Query.EQ("value", "C")
        )
     ));

The shell will verify that we only get the Thing1 we want:

> db.things.find({ children: { $elemMatch : { name:"C3", value:"C"}}}, {name:1, _id:0})
{ "Name" : "Thing1" }

Not an easy bug to catch in a test without a robust set of test data.