One project I’m working on translates a favorite party game among my friends into a SMS-based version that can be played anywhere. A requirement for the project is being able to manage the game state across players. To do this I created a DynamoDB record for each game that maintains the state of the game. Each player has an entry in a Map attribute, which stores data in key-value pairs, with details like their name and score. Throughout the game the score for players needs to be updated by accessing the nested Map attribute of each player. This post will explain a pattern for how you can dynamically build a pattern for updating individual data elements within DynamoDB Maps.
Setting Up an Example
Let's suppose we have a DynamoDB record for a game where a game_id
uniquely identify the game and there is a players
Map attribute containing all the information about each player, in this case just a score
, like the example record below. In this example there are two players, Charlotte and Becky, who each have their own record nested in the players
Map containing the player's score. As the game progresses the score for each player needs to be updated.
Item={
"game_id": "test",
"players": {
"Charlotte": {
"score": 0
},
"Becky": {
"score": 0
}
}
}
Building the Update Pattern
As the game progresses the score
attribute needs to be updated for each player. The function below shows how the DynamoDB update_item
function can be used to update new nested Map attributes.
def update_player_score(game_id, player_id, score_val):
dynamo = boto3.resource('dynamodb')
tbl = dynamo.Table('<TableName>')
result = tbl.update_item(
Key={
"game_id": game_id
},
UpdateExpression="SET players.#player_id.score = :score_val",
ExpressionAttributeNames={
"#player_id": player_id
},
ExpressionAttributeValues={
":score_val": score_val
}
)
The function takes parameters for the game_id
, player_id
, and score_val
. Given those parameters the function will update the player's score to be equal to what is provided in score_val
. For example, if you were to call, update_player_score("test", "Charlotte", 1)
we would change the score of Charlotte to be 1. Let's walkthrough how that happens.
The first part of update_item
. defines what key we will be updating. In this case game_id
is the Key to the table, so we'll be updating records associated with the "test" game_id
.
Next the UpdateExpression
is defined. In the UpdateExpression
we define the pattern for the value we want to update. For this function we want to update the nested parameters within the players
map that could vary by the name of the player. In UpdateExpression
the name of the players
map is hardcoded, but accessing the nested Map attribute needs to be dynamic. There are two placeholders, #player_id
and :score_val
, in UpdateExpression
defining the location of the specific attribute in the map that needs to be updated and the value to set to that nested attributed. The subsequent ExpressionAttributeNames
and ExpressionAttributeValues
definitions provide a mapping to what those placeholders should be.
The values provided in ExpressionAttributeNames
and ExpressionAttributeValues
will replace the placeholders that are in UpdateExpression
when the function is called. For example consider the case where Charlotte's score becomes 1. When the function is called the UpdateExpression
would be equivalent to SET players.Charlotte.score = 1
after the substitutions for #player_id
and :score_val
are made from ExpressionAttributeNames
and ExpressionAttributeValues
.
In DynamoDB an ExpressionAttributeNames is the placeholder for dynamic attribute values. When constructing the UpdateExpression
all attribute values must be prefaced with a #
. In our example we want to update the score for a player, but since the player that's being updated is dynamic the ExpressionAttributeNames
fills in the #player_id
placeholder within UpdateExpression
.
Similarly, ExpressionAttributeValues are placeholders for when the the value of an attribute are not known until runtime. In our case the score to assign to the player. All ExpressionAttributeValues
are prefaced with a :
.
If we wanted to update Becky's score to give her 3 points we could use this function call and the UpdateExpression
would handle the proper mapping to where Becky is located in the players
attribute map:
update_player_score("test", "Becky", 3)
And that’s it! Patterns similar to this are helpful when you have Map attributes in DynamoDB where the path to the attribute you want to update will be dynamoic. When working with Map attributes in DynamoDB it's important to define the ExpressionAttributeNames
and ExpressionAttributeValues
that will be dynamic when building your UpdateExpression
then construct the expression accordingly. Hopefully this was a helpful exercise for you! There are a couple more projects I’m working on that I can’t wait to share so stay tuned!
Top comments (2)
Interesting article..! Thank you for posting it
Curious to know why you didn’t select a schema where game id is the partition key and player is is the sort key. That way your writes would be sharded and you could get a query to read the two items. Assuming that this is a write heavy application.
Yes! In this use case you could definitely include the player as a kind of sort key in the design.