Update or Append to DynamoDB Attributes

28 February 2023

Updated: 03 September 2023

DynamoDB Overview

DynamoDB is AWS’s No SQL Database Service. Dynamo uses partition keys and sort keys to uniquely identify and partion item in the database which allows for high scalability and throughput

When working with traditional SQL databases a common operation is to update the value of a specific column without having to first fetch the entire row. DynamoDB offers us similar functionality using the AWS SDK

The Update Command

DynamoDB commands are simple objects that consist of a few different parts. When looking to update an item the following are relavant:

  1. Key - which is an object that uniquely identifies an item in the database
  2. UpdateExpression - which is an expression that describes the upadate operation to be done using placeholders for attribute names and values (AWS Documentation - Update Expressions)
  3. ExpressionAttributeNames - generic placehodlders for attribute names
  4. ExpresssionAtributeValues - generic placeholders for values

When defining UpdateExpression we use placeholders for the attribute names and values to prevent any escaping/unsupported character related issues. A common convention for this is to use # at the start of attribute names and : at the start of attribute values to make expressions a bit easier to negotiate, for example, if I wanted to set name to bob I would do '#name': 'name' in the ExpressionAttributeNames and '#name': 'bob' in the ExpressionAttributeValues

DynamoDB uses an object representation that is a bit inconvenient to work with, we can use the marshall and unmarshall functions from @aws-sdk/util-dynamodb to simplify things a bit but if you’d like to know more about the data format used you can look towards the end of this post

Our Example

For our example, imagine we have a table of user check-ins with data structured as follows:

1
type Item = {
2
// partition key
3
group: number
4
// sort key
5
username: string
6
7
status: string
8
checkIns: {
9
place: string
10
time: number
11
}[]
12
}

Update an Attribute

We can use a bit of a generic structure to outline our data that we plan to update a single attribute. To do this, we can use the SET command

The update expression for setting a value will look like so:

1
SET #attr = :value

Yup, that’s pretty much it as far as the expression goes, the actual names of the fields we want to update aren’t relevant here, instead they’re mentioned in the ExpressionAtrributeNames and ExpressionAttributeValues

The overall command with all of that is as follows:

1
import { DynamoDBClient } from '@aws-sdk/client-dynamodb'
2
import { marshall, unmarshall } from '@aws-sdk/util-dynamodb'
3
4
const client = new DynamoDBClient({})
5
6
const pk = 'bob'
7
const sk = 12
8
const updateValue = 'available'
9
10
const command = new UpdateItemCommand({
11
TableName: 'my-table-name',
12
Key: marshall({
13
username: pk,
14
group: sk,
15
}),
16
UpdateExpression: 'SET #attr = :value',
17
ExpressionAttributeNames: {
18
'#attr': 'status',
19
},
20
ExpressionAtrributeValues: marshall({
21
':value': updateValue,
22
}),
23
})
24
25
await client.send()

And that’s pretty much the process for updating an attribute witha specific value

Append to a List Attribute That Exists

In the above data structure we have the checkIns field which is a list of objects

We can use the same method as above to define the UpdateExpression, however this time we can use the list_append function in our expression to state that we would like to append a value to

The list_append function appends one list to another - this means that it needs two lists to operate on. To update a list we can just use itself as the first input and the new list as the second. The UpdateExpression looks like this:

1
SET #attr = list_append(#attr, :value)

For our sake we only want to append a single item, so we can just wrap it in an array when we pass it on in the command. The command for the above update looks like so:

1
import { DynamoDBClient } from '@aws-sdk/client-dynamodb'
2
import { marshall, unmarshall } from '@aws-sdk/util-dynamodb'
3
4
const client = new DynamoDBClient({})
5
6
const pk = 'bob'
7
const sk = 12
8
const updateValue = {
9
place: 'home',
10
time: Date.now(),
11
}
12
13
const command = new UpdateItemCommand({
14
TableName: 'my-table-name',
15
Key: marshall({
16
username: pk,
17
group: sk,
18
}),
19
UpdateExpression: 'SET #attr = list_append(#attr, :value)',
20
ExpressionAttributeNames: {
21
'#attr': 'checkIns',
22
},
23
ExpressionAtrributeValues: marshall({
24
// array since the update expression works on two lists
25
':value': [updateValue],
26
}),
27
})
28
29
await client.send()

Append to a List Attribute That May Not Exist

In some cases, we can end up trying to append to an attribute that may not exist, under these circumstances we can use the is_not_exists function that takes an attribute name and a fallback value and will return the fallback if the attribute does not exist in the item

We can change the UpdateExpression to make use of this as follows:

1
SET #attr = list_append(if_not_exists(#attr, :fallback), :value)

And the full command looks like so:

1
import { DynamoDBClient } from '@aws-sdk/client-dynamodb'
2
import { marshall, unmarshall } from '@aws-sdk/util-dynamodb'
3
4
const client = new DynamoDBClient({})
5
6
const pk = 'bob'
7
const sk = 12
8
const updateValue = {
9
place: 'home',
10
time: Date.now(),
11
}
12
13
const command = new UpdateItemCommand({
14
TableName: 'my-table-name',
15
Key: marshall({
16
username: pk,
17
group: sk,
18
}),
19
UpdateExpression:
20
'SET #attr = list_append(if_not_exists(#attr, :fallback), :value)',
21
ExpressionAttributeNames: {
22
'#attr': 'checkIns',
23
},
24
ExpressionAtrributeValues: marshall({
25
':value': [updateValue],
26
// fallback to an empty array if the value does not exist before appending
27
':fallback': [],
28
}),
29
})
30
31
await client.send()

The above expresson helps us append an item to the list while also providing a fallback for the case where the list item may not exist

A Note on Marshalled/Unmarshalled data

DynamoDB works with data in the “marshalled” form, which is an object representation for primitive data types (AWS Documentation - Attribute Value). Some examples of marshalled and unmarshalled data can be seen below:

1
// STRING
2
// Unmarshalled
3
"hello"
4
5
// Marshalled
6
{
7
"S": "Hello"
8
}
9
10
// NUMBER
11
// Unmarshalled
12
25
13
14
// Marshalled
15
{
16
"N": "25"
17
}
18
19
20
// MAP
21
// Unmarshalled
22
{
23
name: "bob"
24
}
25
26
/// Marshalled
27
{
28
"M":{
29
"name":{
30
"S":"hello"
31
}
32
}
33
}

Additional Resources

Speaking of DynamoDB updates, it looks like there’s a library for building queries which seems promising and may bwe worth taking a look at called ElectroDB