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.
#Use the $ Operator to Update the First Match in an Array
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:
1 { 2 "_id": "red", 3 "pokemon": [ 4 { 5 "number": 6, 6 "name": "Charizard" 7 } 8 { 9 "number": 25, 10 "name": "Pikachu", 11 }, 12 { 13 "number": 0, 14 "name": "MissingNo" 15 } 16 ] 17 }
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:
1 db.pokemon_game.update( 2 { "pokemon.name": "MissingNo" }, 3 { 4 "$set": { 5 "pokemon.$.name": "Agumon" 6 } 7 } 8 );
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:
1 { 2 "_id": "red", 3 "pokemon": [ 4 { 5 "number": 6, 6 "name": "Charizard" 7 } 8 { 9 "number": 25, 10 "name": "Pikachu", 11 }, 12 { 13 "number": 0, 14 "name": "Agumon" 15 } 16 ] 17 }
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.
#Use the $[] Operator to Update All Array Elements Within a Document
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:
1 { 2 "_id": "red", 3 "team": [ 4 { 5 "number": 1, 6 "name": "Bulbasaur", 7 "xp": 5 8 }, 9 { 10 "number": 25, 11 "name": "Pikachu", 12 "xp": 32 13 } 14 ] 15 }
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:
1 db.pokemon_game.update( 2 { "_id": "red" }, 3 { 4 "$inc": { 5 "team.$[].xp": 10 6 } 7 } 8 );
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:
1 [ 2 { 3 "_id": "red", 4 "team": [ 5 { 6 "number": 1, 7 "name": "Bulbasaur", 8 "xp": 15 9 }, 10 { 11 "number": 25, 12 "name": "Pikachu", 13 "xp": 42 14 } 15 ] 16 } 17 ]
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.
#Use the $[<identifier>] Operator to Update Elements that Match a Filter Condition
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:
1 db.pokemon_collection.insertMany( 2 [ 3 { 4 _id: "nraboy", 5 cards: [ 6 { 7 "name": "Charizard", 8 "set": "Base", 9 "variant": "1st Edition", 10 "value": 200000 11 }, 12 { 13 "name": "Pikachu", 14 "set": "Base", 15 "variant": "Red Cheeks", 16 "value": 300 17 } 18 ] 19 }, 20 { 21 _id: "mraboy", 22 cards: [ 23 { 24 "name": "Pikachu", 25 "set": "Base", 26 "variant": "Red Cheeks", 27 "value": 300 28 }, 29 { 30 "name": "Pikachu", 31 "set": "McDonalds 25th Anniversary Promo", 32 "variant": "Holo", 33 "value": 10 34 } 35 ] 36 } 37 ] 38 );
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:
1 db.pokemon_collection.update( 2 {}, 3 { 4 "$set": { 5 "cards.$[elemX].value": 350, 6 "cards.$[elemY].value": 500000 7 } 8 }, 9 { 10 "arrayFilters": [ 11 { 12 "elemX.name": "Pikachu", 13 "elemX.set": "Base", 14 "elemX.variant": "Red Cheeks" 15 }, 16 { 17 "elemY.name": "Charizard", 18 "elemY.set": "Base", 19 "elemY.variant": "1st Edition" 20 } 21 ], 22 "multi": true 23 } 24 );
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:
1 [ 2 { 3 "_id": "nraboy", 4 "cards": [ 5 { 6 "name": "Charizard", 7 "set": "Base", 8 "variant": "1st Edition", 9 "value": 500000 10 }, 11 { 12 "name": "Pikachu", 13 "set": "Base", 14 "variant": "Red Cheeks", 15 "value": 350 16 } 17 ] 18 }, 19 { 20 "_id": "mraboy", 21 "cards": [ 22 { 23 "name": "Pikachu", 24 "set": "Base", 25 "variant": "Red Cheeks", 26 "value": 350 27 }, 28 { 29 "name": "Pikachu", 30 "set": "McDonalds 25th Anniversary Promo", 31 "variant": "Holo", 32 "value": 10 33 } 34 ] 35 } 36 ]
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.
#Conclusion
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.