The first access pattern you address in this module is adding new users to a game.
When adding a new user to a game, you need to:
UserGameMapping
entity to add the user to the game.people
attribute on the Game
entity to track how many players are in the game.Note that accomplishing all of these things requires write actions across the existing Game
entity and the new UserGameMapping
entity as well as conditional logic for each of the entities. This is the kind of operation that is a perfect fit for DynamoDB transactions because you need to work on multiple entities in the same request, and you want the entire request to succeed or fail together.
In the code you downloaded, a join_game.py script is in the scripts/ directory. The function in that script uses a DynamoDB transaction to add a user to a game.
import boto3
dynamodb = boto3.client('dynamodb')
GAME_ID = "c6f38a6a-d1c5-4bdf-8468-24692ccc4646"
USERNAME = 'vlopez'
def join_game_for_user(game_id, username):
try:
resp = dynamodb.transact_write_items(
TransactItems=[
{
"Put": {
"TableName": "battle-royale",
"Item": {
"PK": {"S": f"GAME#{game_id}" },
"SK": {"S": f"USER#{username}" },
"game_id": {"S": game_id },
"username": {"S": username }
},
"ConditionExpression": "attribute_not_exists(SK)",
"ReturnValuesOnConditionCheckFailure": "ALL_OLD"
},
},
{
"Update": {
"TableName": "battle-royale",
"Key": {
"PK": { "S": f"GAME#{game_id}" },
"SK": { "S": f"#METADATA#{game_id}" },
},
"UpdateExpression": "SET people = people + :p",
"ConditionExpression": "people < :limit",
"ExpressionAttributeValues": {
":p": { "N": "1" },
":limit": { "N": "50" }
},
"ReturnValuesOnConditionCheckFailure": "ALL_OLD"
}
}
]
)
print(f"Added user: {username} to game: {game_id}")
return True
except Exception as e:
print("Could not add user to game")
join_game_for_user(GAME_ID, USERNAME)
In this script’s join_game_for_user
function, the transact_write_items()
method performs a write transaction. This transaction has two operations.
In the transaction’s first operation, you use a Put
operation to insert a new UserGameMapping
entity. As part of that operation, you specify a condition that the SK
attribute should not exist for this entity. This ensures that an entity with this PK
and SK
doesn’t already exist. If such an entity did already exist, that would mean this user already joined the game.
The second operation is an Update
operation on the Game
entity to increment the people
attribute by one. As part of this operation, you add a conditional check that the current value of people
is not greater than 50. As soon as 50 people join a game, the game is full and ready to begin.
Before we add vlopez
to the game, we can verify the current number of users already in the game by querying the sparse GSI we made. In the AWS DynamoDB console choose Explore items
on the left and filter for the table named Battle Royale
. Choose Query and then select the GSI named OpenGamesIndex from the dropdown. Specify Urban Underground
as the value for the map (Partition Key)
and click the orange Run button. You should see a single item returned with a vale of 49 for the people
attribute.
You can choose to run either the join_game.py
python script or the AWS CLI command below. Both are provided to show different methods of interacting with DynamoDB.
Run this script with the following command in your terminal:
python scripts/join_game.py
The output in your terminal should indicate that the user was added to the game.
Added user: vlopez to game: c6f38a6a-d1c5-4bdf-8468-24692ccc4646
You can return to the DynamoDB console and click Run
again to query the GSI and you will see that the people
attribute now shows 50
Note that if you try to run the script again, the function fails. User vlopez
has been added to the game already, so trying to add the user again does not satisfy the conditions you specified.
Alternatively, you can also submit transactions via the AWS CLI.
Run the following command to add user ebarton
to a game using the Juicy Jungle
map:
aws dynamodb transact-write-items \
--transact-items \
"[
{
\"Put\": {
\"TableName\": \"battle-royale\",
\"Item\": {
\"PK\": {\"S\": \"GAME#248dd9ef-6b17-42f0-9567-2cbd3dd63174\" },
\"SK\": {\"S\": \"USER#ebarton\" },
\"game_id\": {\"S\": \"248dd9ef-6b17-42f0-9567-2cbd3dd63174\" },
\"username\": {\"S\": \"ebarton\" }
},
\"ConditionExpression\": \"attribute_not_exists(SK)\",
\"ReturnValuesOnConditionCheckFailure\": \"ALL_OLD\"
}
},
{
\"Update\": {
\"TableName\": \"battle-royale\",
\"Key\": {
\"PK\": { \"S\": \"GAME#248dd9ef-6b17-42f0-9567-2cbd3dd63174\" },
\"SK\": { \"S\": \"#METADATA#248dd9ef-6b17-42f0-9567-2cbd3dd63174\" }
},
\"UpdateExpression\": \"SET people = people + :p\",
\"ConditionExpression\": \"people < :limit\",
\"ExpressionAttributeValues\": {
\":p\": { \"N\": \"1\" },
\":limit\": { \"N\": \"50\" }
},
\"ReturnValuesOnConditionCheckFailure\": \"ALL_OLD\"
}
}
]"
The addition of DynamoDB transactions greatly simplifies the workflow around complex operations like these. Without transactions, this would have required multiple API calls with complex conditions and manual rollbacks in the event of conflicts. Now, you can implement such complex operations with fewer than 50 lines of code.