Skip to content

Commit 30cc28a

Browse files
authored
Support for UpdateExpression in DDB Enhanced (#3036)
1 parent bf04515 commit 30cc28a

File tree

52 files changed

+5304
-704
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+5304
-704
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"category": "Amazon DynamoDB Enhanced Client",
3+
"contributor": "",
4+
"type": "feature",
5+
"description": "Introducing the UpdateExpression API in the enhanced client and the capability to use update expressions in the extension framework. Additionally, the new AtomicCounterExtension utilizes this feature to provide atomic counter support for users through tagging and annotations."
6+
}

docs/design/services/dynamodb/high-level-library/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
**Design:** New Feature, **Status:** [Public Preview](../../../../../services-custom/dynamodb-enhanced/README.md)
1+
**Design:** New Feature, **Status:** [Released](../../../../../services-custom/dynamodb-enhanced/README.md)
22

33
## Tenets (unless you know better ones)
44

@@ -33,7 +33,7 @@ This problem is not currently addressed directly in the AWS SDK for Java
3333
2.x by any known third-party tool. In 1.11.x, several solutions exist,
3434
including AWS's own Document and Mapper Clients.
3535

36-
## Proposed Solution
36+
## Proposed Solution [Implemented]
3737

3838
The AWS SDK for Java will add a new "enhanced DynamoDB client" that
3939
provides an alternative to the data-access portion of the generated
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
**Design:** New Feature, **Status:** [Released](../../../../../services-custom/dynamodb-enhanced/README.md)
2+
3+
## Problem
4+
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](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html) operation that is underlying the DynamoDB Enhanced op supports a wider range of functionality through its [UpdateExpression syntax](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html).
5+
6+
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.
7+
8+
### Requested features
9+
Customer-requested features related to DynamoDB Enhanced UpdateItem:
10+
11+
1. Increment/decrement numerical attribute values in order to support atomic counters and similar use cases
12+
2. Adding items to lists
13+
3. Unsetting / nullifying specific attributes while not modifying the whole item.
14+
4. Modifying return value behavior.
15+
16+
Example Github issue: https://github.com/aws/aws-sdk-java-v2/issues/2292
17+
18+
## Current functionality
19+
When calling updateItem, the enhanced client converts the supplied POJO into the low-level UpdateExpression syntax. It supports only a few specific actions:
20+
- REMOVE - to delete attributes
21+
- SET - setting whole attributes
22+
23+
## Proposed Solution
24+
The enhanced client lets users provide custom update expressions in addition to the normal POJO records provided
25+
to the updateItem operation.
26+
### Enhanced Client UpdateExpression API
27+
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.
28+
29+
~~~
30+
SetAction setAction = SetAction.builder()
31+
.path("#attr1_ref")
32+
.value(":new_value")
33+
.putExpressionName("#attr1_ref", "attr1")
34+
.putExpressionValue(":new_value", newValue)
35+
.build();
36+
37+
UpdateExpression updateExpression = UpdateExpression.builder()
38+
.addAction(setAction)
39+
.build();
40+
~~~
41+
*path = either the attribute name or another expression supported by the low level API.*<br>
42+
*value = the value of the path. Can also contain low level expressions.*<br>
43+
*expressionNames = (optional) maps name tokens to attribute names.*<br>
44+
*expressionValues = maps value tokens to attribute values.*<br>
45+
46+
### Applying an UpdateExpression
47+
#### Schema level
48+
Schema level UpdateExpression enable use cases where the same action should be applied every time the database is called, such as atomic counters.
49+
The extension framework supports UpdateExpression as an output in a WriteModification.
50+
51+
In an extension class implementing [DynamoDbEnhancedClientExtension](https://github.com/aws/aws-sdk-java-v2/blob/feature/master/DynamoDBenhanced-updateexpression/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/DynamoDbEnhancedClientExtension.java):
52+
~~~
53+
WriteModification.builder()
54+
.updateExpression(updateExpression)
55+
.build();
56+
~~~
57+
Adding the extension to the extension chain:
58+
~~~
59+
DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder()
60+
.dynamoDbClient(getDynamoDbClient())
61+
.extensions(MyExtension.create())
62+
.build();
63+
64+
DynamoDbTable<Record> table = enhancedClient.table("some-table-name"), TableSchema.fromClass(SomeRecord.class));
65+
~~~
66+
67+
68+
#### Request level
69+
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.
70+
71+
**NOTE:** This feature is not supported in the initial release.
72+
73+
### Transforming enhanced UpdateExpression to low-level
74+
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.
75+
76+
### Precedence and merging rules
77+
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.
78+
79+
[PR #2926](https://github.com/aws/aws-sdk-java-v2/pull/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.
80+
81+
#### Merging UpdateExpression in the extension chain
82+
83+
Extension merge adds later expressions in the chain to any existing UpdateExpression (remember, the UpdateExpression is just a set of collections before parsing).
84+
85+
Q: What happens if one extension later in the chain modifies the same attribute?<br>
86+
A: Their update actions will both be in the UpdateExpression, and it will fail at parse time.
87+
Should we have more support here?
88+
89+
Q: Can an extension see previous UpdateExpression in the extension chain?<br>
90+
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).
91+
92+
#### Merging extension and request POJO UpdateExpressions
93+
94+
Q: What takes precedence, extension expressions or reqest-level POJO extensions?<br>
95+
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.
96+
97+
Q: Should we consider doing something similar for POJO SET operations? <br>
98+
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.
99+
100+
Q: What happens if the POJO sets an attribute that is also modified by an extension?
101+
A: An exception is thrown at parse time, preventing users from overwriting an extension attribute.
102+
103+
## Appendix B: Alternative solutions
104+
105+
### Design alternative: More fluent UpdateExpression API
106+
In this design alternative, the UpdateExpression API and update actions are more fluent, allowing users to write less code to achieve their goals:
107+
~~~
108+
UpdateExpression updateExpression1 = UpdateExpression.builder()
109+
.remove("attr1")
110+
.set("attr1", value1, updateBehavior)
111+
.build();
112+
~~~
113+
If directly adding actions, these had methods exposed
114+
~~~
115+
UpdateExpression updateExpression2 = UpdateExpression.builder()
116+
.removeAction(UpdateAction.remove("attr1"))
117+
.setAction(SetUpdateAction.addWithStartValue(attr2, delta, start))
118+
.addAction(UpdateAction.appendToList("attr1", 3, myListAttributeValue))
119+
.addAction(UpdateAction.appendToList("attr1", 3, myListAttributeValue))
120+
.build();
121+
~~~
122+
123+
**Decision**
124+
125+
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.
126+
127+
### Design alternative: Single UpdateAction class
128+
Using a type field to differentiate between different actions:
129+
~~~
130+
UpdateAction updateAction =
131+
UpdateAction.builder()
132+
.type(UpdateActionType.REMOVE)
133+
.attributeName(attributeName)
134+
.expression(keyRef(attributeName))
135+
.build();
136+
~~~
137+
138+
**Decision**
139+
140+
Discarded in favor of the current design.

services-custom/dynamodb-enhanced/README.md

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -298,9 +298,10 @@ happens. Some operations such as UpdateItem perform both a write and
298298
then a read, so call both hooks.
299299
300300
Extensions are loaded in the order they are specified in the enhanced client builder. This load order can be important,
301-
as one extension can be acting on values that have been transformed by a previous extension. By default, just the
302-
VersionedRecordExtension will be loaded, however you can override this behavior on the client builder and load any
303-
extensions you like or specify none if you do not want the default bundled VersionedRecordExtension.
301+
as one extension can be acting on values that have been transformed by a previous extension. The client comes with a set
302+
of pre-written plugin extensions, located in the `/extensions` package. By default (See ExtensionResolver.java) the client loads some of them,
303+
such as VersionedRecordExtension; however, you can override this behavior on the client builder and load any
304+
extensions you like or specify none if you do not want the ones bundled by default.
304305
305306
In this example, a custom extension named 'verifyChecksumExtension' is being loaded after the VersionedRecordExtension
306307
which is usually loaded by default by itself:
@@ -341,6 +342,27 @@ Or using a StaticTableSchema:
341342
.tags(versionAttribute())
342343
```
343344
345+
### AtomicCounterExtension
346+
347+
This extension is loaded by default and will increment numerical attributes each time records are written to the
348+
database. Start and increment values can be specified, if not counters start at 0 and increments by 1.
349+
350+
To tell the extension which attribute is a counter, tag an attribute of type Long in the TableSchema, here using
351+
standard values:
352+
```java
353+
@DynamoDbAtomicCounter
354+
public Long getCounter() {...};
355+
public void setCounter(Long counter) {...};
356+
```
357+
Or using a StaticTableSchema with custom values:
358+
```java
359+
.addAttribute(Integer.class, a -> a.name("counter")
360+
.getter(Customer::getCounter)
361+
.setter(Customer::setCounter)
362+
// Apply the 'atomicCounter' tag to the attribute with start and increment values
363+
.tags(atomicCounter(10L, 5L))
364+
```
365+
344366
## Advanced table schema features
345367
### Explicitly include/exclude attributes in DDB mapping
346368
#### Excluding attributes

services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/Expression.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,15 @@ public Map<String, String> expressionNames() {
189189
return expressionNames;
190190
}
191191

192+
/**
193+
* Coalesces two complete expressions into a single expression joined by an 'AND'.
194+
*
195+
* @see #join(Expression, Expression, String)
196+
*/
197+
public Expression and(Expression expression) {
198+
return join(this, expression, " AND ");
199+
}
200+
192201
@Override
193202
public boolean equals(Object o) {
194203
if (this == o) {

0 commit comments

Comments
 (0)