DDBTools: DynamoDB UpdateItem with Go the easy way
There doesn’t seem to be a nice clean way of handling DynamoDB (DDB) record updates in Go without writing a DDB query and book-ending it with some boilerplate. The current method that’s documented for developers to adopt in my opinion makes for some less than attractive and readable code. In fact, one could argue the effort to write the update code when moving quickly is enough to make you want to hack it together,.
There are three methods available to you, including one which uses my simple helper library.
The aspiration to write this simple Go library came from frustration of the trial and error of writing large DynamoDB methods. Some of my records are hybrid from the perspective of the UI. What I mean by this is for example, is a user settings form will contain a partial set of properties that the record holds. Merely overwriting the record will destroy the rest of the record, which might have held critical information prior to the overwrite like paymentIDs and other such stuff.
This is an example of a record this short blog post will pivot around.
PK | SK | paymentID | firstName | lastName |
---|---|---|---|---|
“user” | $GUID | $payment_guid | Dave | Gee |
Method 1: Overwrite the entire record with a DynamoDB PutItem query
Even with this there are approaches that could work quite well. As your code receives the payload with the updates, your back-end code can read the current record and then overwrite the properties you wish to retain prior to executing the query. That will cost you a read and a write. In this specific example, that would mean retaining the paymentID
field. Only the firstName
and lastName
should be exposed to the UI as the paymentID
property is generated and managed by the back-end.
If you don’t have this problem of records requiring partial updates, you could just live the YOLO life and overwrite the entire record. That might actually be fine for your use case but at the cost of relying on the data integrity for the record properties you do not wish to change. The surface area for error spans the HTTP calls to and from the API and the UI not messing it up. As a security note, fully formed primary key material (PK and SK) should never be trusted from the UI, but re-formed by the back-end on CRUD operations.
Method2: Update specific properties on the record with a DynamoDB UpdateItem query
This is tedious code in my opinion. The list of values and update string can get quite large and it makes for one ugly read.
package update
import (
"context"
"fmt"
"os"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
)
// These tags are useless for our example
// but for Creating records from struct instances, they are super useful!
type Settings struct {
PK string `dynamodbav:"PK"`
SK string `dynamodbav:"SK"`
PaymentID string `dynamodbav:"paymentID"`
FirstName string `dynamodbav:"firstName"`
LastName string `dynamodbav:"lastName"`
}
func updateItem(input Settings) error {
cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion(os.Getenv("AWS_REGION")))
if err != nil {
return fmt.Errorf("LoadDefaultConfig: %v\n", err)
}
client := dynamodb.NewFromConfig(cfg)
_, err = client.UpdateItem(context.TODO(), &dynamodb.UpdateItemInput{
TableName: aws.String(os.Getenv("DYNAMODB_TABLE")),
Key: map[string]types.AttributeValue{
"PK": &types.AttributeValueMemberS{Value: item.PK},
"SK": &types.AttributeValueMemberS{Value: item.SK},
},
// This block can get really out of hand on big updates
UpdateExpression: aws.String("set firstName = :firstName, lastName = :lastName"),
ExpressionAttributeValues: map[string]types.AttributeValue{
":firstName": &types.AttributeValueMemberS{Value: item.FirstName},
":lastName": &types.AttributeValueMemberS{Value: item.LastName},
},
})
if err != nil {
return fmt.Errorf("UpdateItem: %v\n", err)
}
return nil
}
Also, if you do things like increment numbers with conditions, you’ll find the query is better being handles as a transaction, with multiple parts. That means building for failure conditions because a transaction will either succeed or fail as one.
Method 3: Use a helper function to do updates
The more I understand how DynamoDB works, the more I like it, but the abstraction between the Go v2 SDK is quite thin and it means writing queries almost natively. It does mean however you can debug queries rapidly as the layers are indeed thin. I’ve created a middle ground option for my projects that use DynamoDB. I’ve created a factory function which returns both the ExpressionAttributeValues
map and UpdateExpression
string. I think it’s more readable and means writing update code is faster.
Here’s an example of that factory function in play.
go get github.com/davedotdev/ddbtools@latest
import (
"github.com/davedotdev/ddbtools"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
)
func (da *Data) UpdateSetting(firstName, lastName, userGUID string) error {
client := dynamodb.NewFromConfig(da.DDBConfig, func(o *dynamodb.Options) {})
// The entry needs to exist first and foremost!
CondExpr := "attribute_exists(#SK)"
// Boilerplate
PKKey := "setting"
SKKey := userGUID
upd := dynamodb.UpdateItemInput{}
upd.ConditionExpression = &CondExpr
upd.TableName = &da.TableName
upd.Key = map[string]types.AttributeValue{}
upd.ExpressionAttributeNames = make(map[string]string)
upd.ExpressionAttributeValues = make(map[string]types.AttributeValue)
upd.Key["PK"] = &types.AttributeValueMemberS{Value: PKKey}
upd.Key["SK"] = &types.AttributeValueMemberS{Value: SKKey}
upd.ExpressionAttributeNames["#SK"] = "SK"
// The magic is here: handling each property
// pass in the UpdateItemInput instance (upd) and the string value of exprStr
// If the fourth input is true, then it creates a new string for exprStr
exprStr := ddbtools.SetEquals(firstName, "firstName", "", true, &upd)
exprStr = ddbtools.SetEquals(lastName, "lastName", exprStr, false, &upd)
// This SetEquals func isn't too disruptive to normal patterns
// and doesn't do any real 'magic', so it's easy to debug
upd.UpdateExpression = &exprStr
// Do the transaction
_, err := client.UpdateItem(context.TODO(), &upd)
if err != nil {
return err
}
return nil
}
Wrap-Up
Hope this short post on a DynamoDB is helpful for Go SDK v2 consumers. I found writing updates to be the most tedious and took some of this pain away by writing a small helper lib.
// Dave
- Tags: go, dynamodb
- Categories: go, dynamodb