In this post, I’m going to show how to insert, update or delete a single item from a nested collection property in DynamoDB.
Let’s say we have a document like this.
[DynamoDBTable("authors")]
public class Author
{
[DynamoDBHashKey]
public Guid Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public List<Book> Books { get; set; }
}
public class Book
{
public Guid Id { get; set; }
public string Name { get; set; }
}
One way (somewhat obvious?) to update, insert or delete a particular book from this document is to retrieve the whole document, mutate the data and then save it back. However, the issue with this approach is that if there are concurrent requests, the last in wins. This means the last request would undo the other concurrent requests. For example, if there are two concurrent requests, one to insert a new book and one to delete an existing book. Let’s say the delete existing book comes in last, because we load the whole document at the same time as the other request, it would not have the new book added from the other request yet. Once it mutates the document to delete the book and save changes, it would not have the new book from the other request.
The better solution is to not affect other data in the document when adding, updating or deleting an item. This can be achieved by updating individual item using UpdateItemAsync from AWSSDK.DynamoDBv2.
Insert
To insert, we can use the SET update expression with list_append function
private readonly IAmazonDynamoDB _dbClient;
public async Task InsertBook(Guid authorId, Book book)
{
await _dbClient.UpdateItemAsync(new UpdateItemRequest
{
TableName = "authors",
UpdateExpression = "SET #bk = list_append(#bk, :newBooks)",
Key = new Dictionary<string, AttributeValue>
{
{ "Id", new AttributeValue(authorId.ToString()) },
},
ExpressionAttributeNames = new Dictionary<string, string>
{
{ "#bk", "Books" },
},
ExpressionAttributeValues = new Dictionary<string, AttributeValue>
{
{
":newBooks",
new AttributeValue
{
L = new List<AttributeValue>
{
new AttributeValue
{
M = Document
.FromJson(JsonConvert.SerializeObject(book))
.ToAttributeMap()
}
}
}
},
}
});
}
Update
With update, we will need the index of the book to be updated. Unfortunately, there is a ConditionExpression but DynamoDB does not support updating book where book ID equals something.
public async Task UpdateBook(Guid authorId, Book book, int index)
{
await _dbClient.UpdateItemAsync(new UpdateItemRequest
{
TableName = "authors",
UpdateExpression = $"SET #bk[{index}] = :book",
Key = new Dictionary<string, AttributeValue>
{
{ "Id", new AttributeValue(authorId.ToString()) },
},
ExpressionAttributeNames = new Dictionary<string, string>
{
{ "#bk", "Books" },
},
ExpressionAttributeValues = new Dictionary<string, AttributeValue>
{
{
":book",
new AttributeValue
{
M = Document
.FromJson(JsonConvert.SerializeObject(book))
.ToAttributeMap()
}
},
}
});
}
Delete
Similarly to Update, we also need the index of the book that we want to delete. To delete, we use the REMOVE update expression
public async Task DeleteBook(Guid authorId, int bookIndex)
{
await _dbClient.UpdateItemAsync(new UpdateItemRequest
{
TableName = "authors",
UpdateExpression = $"REMOVE #bk[{bookIndex}]",
Key = new Dictionary<string, AttributeValue>
{
{ "Id", new AttributeValue(authorId.ToString()) },
},
ExpressionAttributeNames = new Dictionary<string, string>
{
{ "#bk", "Books" },
}
});
}
Conclusion
These will update only the item in the document without affecting other data and support concurrent requests.
You can actually combine the update expressions to update/insert/delete multiple items. It’s also possible to perform insert, delete, and update in one go.
SET #bk = list_append(#bk, :newBooks), #bk[1] = :book1, #bk[2] = :book2 REMOVE #bk[3]
The above update expression will add new books, update books at index 1 and 2, and remove book at index 3.