Design: New Feature, Status: Released
The DynamoDB Enhanced updateItem() table operation supports creating or updating an existing item by overwriting some or all attributes when supplying a POJO type instance with key attributes. In contrast, the low level DynamoDB updateItem operation that is underlying the DynamoDB Enhanced op supports a wider range of functionality through its UpdateExpression syntax.
This document proposes a mechanism for users to provide update expressions that will allow them to take advantage of the features of the low level expressions and implement functionality such as atomic counters.
Customer-requested features related to DynamoDB Enhanced UpdateItem:
Example Github issue: https://github.com/aws/aws-sdk-java-v2/issues/2292
When calling updateItem, the enhanced client converts the supplied POJO into the low-level UpdateExpression syntax. It supports only a few specific actions:
The enhanced client lets users provide custom update expressions in addition to the normal POJO records provided to the updateItem operation.
The UpdateExpressions you can write in the enhanced client models the DynamoDB syntax at a higher abstraction level in order to support merging and analyzing expressions. To create an UpdateExpression, create one or more UpdateAction (AddAction, SetAction, RemoveAction and DeleteAction) and add to the UpdateExpression builder.
SetAction setAction = SetAction.builder()
.path("#attr1_ref")
.value(":new_value")
.putExpressionName("#attr1_ref", "attr1")
.putExpressionValue(":new_value", newValue)
.build();
UpdateExpression updateExpression = UpdateExpression.builder()
.addAction(setAction)
.build();
path = either the attribute name or another expression supported by the low level API.
value = the value of the path. Can also contain low level expressions.
expressionNames = (optional) maps name tokens to attribute names.
expressionValues = maps value tokens to attribute values.
Schema level UpdateExpression enable use cases where the same action should be applied every time the database is called, such as atomic counters. The extension framework supports UpdateExpression as an output in a WriteModification.
In an extension class implementing DynamoDbEnhancedClientExtension:
WriteModification.builder()
.updateExpression(updateExpression)
.build();
Adding the extension to the extension chain:
DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder()
.dynamoDbClient(getDynamoDbClient())
.extensions(MyExtension.create())
.build();
DynamoDbTable<Record> table = enhancedClient.table("some-table-name"), TableSchema.fromClass(SomeRecord.class));
Request level UpdateExpression would allow you to modify the record at a single instant in time, such as adding an item to a list or deleting an attribute, by adding an update expression to the request itself.
NOTE: This feature is not supported in the initial release.
Performed by the enhanced client in the UpdateItemOperation when creating the low level request. It uses the UpdateExpressionConverter to transform the UpdateExpression into an Expression and then the string format DynamoDB expects.
Several extensions can return an UpdateExpression, and they need to be merged in the extension chain so that a final extension UpdateExpression reaches the UpdateItemOperation. Within the operation, the extension expression must be merged with the one generated for the request POJO.
PR #2926 outlines the challenges when resolving the final UpdateExpression in further detail. One of the biggest issues is reconciling the user experience in both using extensions and request and getting a result that makes sense with default configuration.
Extension merge adds later expressions in the chain to any existing UpdateExpression (remember, the UpdateExpression is just a set of collections before parsing).
Q: What happens if one extension later in the chain modifies the same attribute?
A: Their update actions will both be in the UpdateExpression, and it will fail at parse time. Should we have more support here?
Q: Can an extension see previous UpdateExpression in the extension chain?
A: The extension context, input to the extension, does not currently contain UpdateExpression, which means the extension cannot view previous expressions (The extension CAN view any updates to the transact item however).
Q: What takes precedence, extension expressions or reqest-level POJO extensions?
A: We currently just add them together, with one exception: A filter is in place that removes automatically generated delete of attributes for the POJO expression, that are explicitly modified by the extension UpdateExpression. This is because ignoreNulls is false by default and remove statements would be created to collide with extension attributes.
Q: Should we consider doing something similar for POJO SET operations?
A: It depends on the view one takes of request level POJO updates compared to the extension ones. On one hand, request level often takes precedence. On the other, it’s important to protect the extension from being overwritten. This is not implemented.
Q: What happens if the POJO sets an attribute that is also modified by an extension? A: An exception is thrown at parse time, preventing users from overwriting an extension attribute.
In this design alternative, the UpdateExpression API and update actions are more fluent, allowing users to write less code to achieve their goals:
UpdateExpression updateExpression1 = UpdateExpression.builder()
.remove("attr1")
.set("attr1", value1, updateBehavior)
.build();
If directly adding actions, these had methods exposed
UpdateExpression updateExpression2 = UpdateExpression.builder()
.removeAction(UpdateAction.remove("attr1"))
.setAction(SetUpdateAction.addWithStartValue(attr2, delta, start))
.addAction(UpdateAction.appendToList("attr1", 3, myListAttributeValue))
.addAction(UpdateAction.appendToList("attr1", 3, myListAttributeValue))
.build();
Decision
This alternative was discarded due to the lack of flexibility in the highly modeled API risking difficulties in keeping up with changes to the low level syntax by DynamoDB.
Using a type field to differentiate between different actions:
UpdateAction updateAction =
UpdateAction.builder()
.type(UpdateActionType.REMOVE)
.attributeName(attributeName)
.expression(keyRef(attributeName))
.build();
Decision
Discarded in favor of the current design.