MongoDB offers a rich query language that’s great for create, read, update, and delete operations as well as complex multi-stage aggregation pipelines. There are many ways to model your data within MongoDB and regardless of how it looks, the MongoDB Query Language (MQL) has you covered.
One of the lesser recognized but extremely valuable features of MQL is in the positional operators that you’d find in an update operation.
Let’s say that you have a document and inside that document, you have an array of objects. You need to update one or more of those objects in the array, but you don’t want to replace the array or append to it. This is where a positional operator might be valuable.
In this tutorial, we’re going to look at a few examples that would benefit from a positional operator within MongoDB.
Let’s use the example that we have an array in each of our documents and we want to update only the first match within that array, even if there’s a potential for numerous matches.
To do this, we’d probably want to use the $
operator which acts as a placeholder to update the first element matched.
For this example, let’s use an old-school Pokemon video game. Take look at the following MongoDB document:
{
"_id": "red",
"pokemon": [
{
"number": 6,
"name": "Charizard"
},
{
"number": 25,
"name": "Pikachu",
},
{
"number": 0,
"name": "MissingNo"
}
]
}
Let’s assume that the above document represents the Pokemon information for the Pokemon Red video game. The document is not a true reflection and it is very much incomplete. However, if you’re a fan of the game, you’ll probably remember the glitch Pokemon named “MissingNo.” To make up a fictional story, let’s assume the developer, at some point in time, wanted to give that Pokemon an actual name, but forgot.
We can update that particular element in the array by doing something like the following:
db.pokemon_game.update(
{ "pokemon.name": "MissingNo" },
{
"$set": {
"pokemon.$.name": "Agumon"
}
}
);
In the above example, we are doing a filter for documents that have an array element with a name
field set to MissingNo
. With MongoDB, you don’t need to specify the array index in your filter for the update
operator. In the manipulation step, we are using the $
positional operator to change the first occurrence of the match in the filter. Yes, in my example, I am renaming the “MissingNo” Pokemon to that of a Digimon, which is an entirely different brand.
The new document would look like this:
{
"_id": "red",
"pokemon": [
{
"number": 6,
"name": "Charizard"
},
{
"number": 25,
"name": "Pikachu",
},
{
"number": 0,
"name": "Agumon"
}
]
}
Had “MissingNo” appeared numerous times within the array, only the first occurrence would be updated. If “MissingNo” appeared numerous times, but the surrounding fields were different, you could match on multiple fields using the $elemMatch
operator to narrow down which particular element should be updated.
More information on the $
positional operator can be found in the documentation.
Let’s say that you have an array in your document and you need to update every element in that array using a single operation. To do this, we might want to take a look at the $[]
operator which does exactly that.
Using the same Pokemon video game example, let’s imagine that we have a team of Pokemon and we’ve just finished a battle in the game. The experience points gained from the battle need to be distributed to all the Pokemon on your team.
The document that represents our team might look like the following:
{
"_id": "red",
"team": [
{
"number": 1,
"name": "Bulbasaur",
"xp": 5
},
{
"number": 25,
"name": "Pikachu",
"xp": 32
}
]
}
At the end of the battle, we want to make sure every Pokemon on our team receives 10 XP. To do this with the $[]
operator, we can construct an update
operation that looks like the following:
db.pokemon_game.update(
{ "_id": "red" },
{
"$inc": {
"team.$[].xp": 10
}
}
);
In the above example, we use the $inc
modifier to increase all xp
fields within the team
array by a constant number. To learn more about the $inc
operator, check out the documentation.
Our new document would look like this:
[
{
"_id": "red",
"team": [
{
"number": 1,
"name": "Bulbasaur",
"xp": 15
},
{
"number": 25,
"name": "Pikachu",
"xp": 42
}
]
}
]
While useful for this example, we don’t exactly get to provide criteria in case one of your Pokemon shouldn’t receive experience points. If your Pokemon has fainted, maybe they shouldn’t get the increase.
We’ll learn about filters in the next part of the tutorial.
To learn more about the $[]
operator, check out the documentation.
Let’s use the example that we have several array elements that we want to update in a single operation and we don’t want to worry about excessive client-side code paired with a replace operation.
To do this, we’d probably want to use the $[<identifier>]
operator which acts as a placeholder to update all elements that match an arrayFilters
condition.
To put things into perspective, let’s say that we’re dealing with Pokemon trading cards, instead of video games, and tracking their values. Our documents might look like this:
db.pokemon_collection.insertMany(
[
{
_id: "nraboy",
cards: [
{
"name": "Charizard",
"set": "Base",
"variant": "1st Edition",
"value": 200000
},
{
"name": "Pikachu",
"set": "Base",
"variant": "Red Cheeks",
"value": 300
}
]
},
{
_id: "mraboy",
cards: [
{
"name": "Pikachu",
"set": "Base",
"variant": "Red Cheeks",
"value": 300
},
{
"name": "Pikachu",
"set": "McDonalds 25th Anniversary Promo",
"variant": "Holo",
"value": 10
}
]
}
]
);
Of course, the above snippet isn’t a document, but an operation to insert two documents into some pokemon_collection
collection within MongoDB. In the above scenario, each document represents a collection of cards for an individual. The cards
array has information about the card in the collection as well as the current value.
In our example, we need to update prices of cards, but we don’t want to do X number of update operations against the database. We only want to do a single operation to update the values of each of our cards.
Take the following query:
db.pokemon_collection.update(
{},
{
"$set": {
"cards.$[elemX].value": 350,
"cards.$[elemY].value": 500000
}
},
{
"arrayFilters": [
{
"elemX.name": "Pikachu",
"elemX.set": "Base",
"elemX.variant": "Red Cheeks"
},
{
"elemY.name": "Charizard",
"elemY.set": "Base",
"elemY.variant": "1st Edition"
}
],
"multi": true
}
);
The above update
operation is like any other, but with an extra step for our positional operator. The first parameter, which is an empty object, represents our match criteria. Because it is empty, we’ll be updating all documents within the collection.
The next parameter is the manipulation we want to do to our documents. Let’s skip it for now and look at the arrayFilters
in the third parameter.
Imagine that we want to update the price for two particular cards that might exist in any person’s Pokemon collection. In this example, we want to update the price of the Pikachu and Charizard cards. If you’re a Pokemon trading card fan, you’ll know that there are many variations of the Pikachu and Charizard card, so we get specific in our arrayFilters
array. For each object in the array, the fields of those objects represent an and
condition. So, for elemX
, which has no specific naming convention, all three fields must be satisfied.
In the above example, we are using elemX
and elemY
to represent two different filters.
Let’s go back to the second parameter in the update
operation. If the filter for elemX
comes back as true because an array item in a document matched, then the value
field for that object will be set to a new value. Likewise, the same thing could happen for the elemY
filter. If a document has an array and one of the filters does not ever match an element in that array, it will be ignored.
If looking at our example, the documents would now look like the following:
[
{
"_id": "nraboy",
"cards": [
{
"name": "Charizard",
"set": "Base",
"variant": "1st Edition",
"value": 500000
},
{
"name": "Pikachu",
"set": "Base",
"variant": "Red Cheeks",
"value": 350
}
]
},
{
"_id": "mraboy",
"cards": [
{
"name": "Pikachu",
"set": "Base",
"variant": "Red Cheeks",
"value": 350
},
{
"name": "Pikachu",
"set": "McDonalds 25th Anniversary Promo",
"variant": "Holo",
"value": 10
}
]
}
]
If any particular array contained multiple matches for one of the arrayFilter
criteria, all matches would have their price updated. This means that if I had, say, 100 matching Pikachu cards in my Pokemon collection, all 100 would now have new prices.
More information on the $[<identifier>]
operator can be found in the documentation.
You just saw how to use some of the positional operators within the MongoDB Query Language (MQL). These operators are useful when working with arrays because they prevent you from having to do full replaces on the array or extended client-side manipulation.
To learn more about MQL, check out my previous tutorial titled, Getting Started with Atlas and the MongoDB Query Language (MQL).
If you have any questions, take a moment to stop by the MongoDB Community Forums.
This content first appeared on MongoDB.