Skip to content

Commit baee3b7

Browse files
committed
Fix handling of UpdateBehavior.WRITE_IF_NOT_EXISTS on @DynamoDbAutoGeneratedUuid
1 parent 5dac489 commit baee3b7

File tree

4 files changed

+348
-53
lines changed

4 files changed

+348
-53
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "bugfix",
3+
"category": "Amazon DynamoDB Enhanced Client",
4+
"contributor": "",
5+
"description": "Fix handling of UpdateBehavior.WRITE_IF_NOT_EXISTS on @DynamoDbAutoGeneratedUuid"
6+
}

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

Lines changed: 47 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -29,51 +29,53 @@
2929
import software.amazon.awssdk.enhanced.dynamodb.EnhancedType;
3030
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTag;
3131
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableMetadata;
32-
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbUpdateBehavior;
3332
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
33+
import software.amazon.awssdk.utils.StringUtils;
3434
import software.amazon.awssdk.utils.Validate;
3535

3636

3737
/**
38-
* This extension facilitates the automatic generation of a unique UUID (Universally Unique Identifier) for a specified attribute
39-
* every time a new record is written to the database. The generated UUID is obtained using the
40-
* {@link java.util.UUID#randomUUID()} method.
41-
* <p>
42-
* This extension is not loaded by default when you instantiate a
43-
* {@link software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient}. Therefore, you need to specify it in a custom
44-
* extension when creating the enhanced client.
45-
* <p>
46-
* Example to add AutoGeneratedUuidExtension along with default extensions is
47-
* {@snippet :
48-
* DynamoDbEnhancedClient.builder().extensions(Stream.concat(ExtensionResolver.defaultExtensions().stream(),
49-
* Stream.of(AutoGeneratedUuidExtension.create())).collect(Collectors.toList())).build();
50-
*}
51-
* </p>
52-
* <p>
53-
* Example to just add AutoGeneratedUuidExtension without default extensions is
54-
* {@snippet :
55-
* DynamoDbEnhancedClient.builder().extensions(AutoGeneratedUuidExtension.create()).build();
56-
*}
57-
* </p>
58-
* <p>
59-
* To utilize the auto-generated UUID feature, first, create a field in your model that will store the UUID for the attribute.
60-
* This class field must be of type {@link java.lang.String}, and you need to tag it as the autoGeneratedUuidAttribute. If you are
61-
* using the {@link software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema}, then you should use the
62-
* {@link software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedUuid} annotation. If you are using
63-
* the {@link software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema}, then you should use the
64-
* {@link
65-
* software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension.AttributeTags#autoGeneratedUuidAttribute()}
66-
* static attribute tag.
67-
* </p>
68-
* <p>
69-
* Every time a new record is successfully put into the database, the specified attribute will be automatically populated with a
70-
* unique UUID generated using {@link java.util.UUID#randomUUID()}. If the UUID needs to be created only for `putItem` and should
71-
* not be generated for an `updateItem`, then
72-
* {@link software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior#WRITE_IF_NOT_EXISTS} must be along with
73-
* {@link DynamoDbUpdateBehavior}
38+
* This extension facilitates the automatic generation of a unique UUID (Universally Unique Identifier) for attributes tagged with
39+
* {@code @DynamoDbAutoGeneratedUuid} or {@link AutoGeneratedUuidExtension.AttributeTags#autoGeneratedUuidAttribute()}. The
40+
* generated UUID is obtained using {@link java.util.UUID#randomUUID()}.
7441
*
75-
* </p>
42+
* <p>Usage:</p>
43+
* <ul>
44+
* <li>This extension is not loaded by default; register it explicitly when building
45+
* a {@link software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient}.</li>
46+
* <li>The annotated attribute must be of type {@link String}.</li>
47+
* <li>If the attribute is <b>missing or empty</b> in the item being written, a UUID
48+
* will be generated and set in the outgoing request map.</li>
49+
* <li>If the attribute already has a non-empty value, it is preserved and not overwritten.</li>
50+
* </ul>
51+
*
52+
* <p><b>Update behavior:</b></p>
53+
* <ul>
54+
* <li>With the default {@link software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior#WRITE_ALWAYS},
55+
* a missing attribute on an update will cause a new UUID to be generated and written.</li>
56+
* <li>With {@link software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior#WRITE_IF_NOT_EXISTS},
57+
* a missing attribute on an update will cause a UUID to be generated in the outgoing map,
58+
* but DynamoDB will preserve the existing stored value thanks to the conditional
59+
* <code>if_not_exists(...)</code> expression the mapper generates.</li>
60+
* </ul>
61+
*
62+
* <p><b>Difference between putItem and updateItem:</b></p>
63+
* <ul>
64+
* <li>{@code putItem} always replaces the entire item. If the field is absent in the payload, the extension
65+
* will generate a new UUID, even when {@code WRITE_IF_NOT_EXISTS} is specified.</li>
66+
* <li>{@code updateItem} respects {@code WRITE_IF_NOT_EXISTS}: if the attribute already exists in DynamoDB,
67+
* the previously stored value is preserved and a new UUID will not overwrite it.</li>
68+
* </ul>
69+
*
70+
* <p>Examples:</p>
71+
* <pre>{@code
72+
* DynamoDbEnhancedClient client = DynamoDbEnhancedClient.builder()
73+
* .dynamoDbClient(lowLevelClient)
74+
* .extensions(AutoGeneratedUuidExtension.create())
75+
* .build();
76+
* }</pre>
7677
*/
78+
7779
@SdkPublicApi
7880
@ThreadSafe
7981
public final class AutoGeneratedUuidExtension implements DynamoDbEnhancedClientExtension {
@@ -110,15 +112,18 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex
110112
}
111113

112114
Map<String, AttributeValue> itemToTransform = new HashMap<>(context.items());
113-
customMetadataObject.forEach(key -> insertUuidInItemToTransform(itemToTransform, key));
115+
customMetadataObject.forEach(key -> insertUuidIfMissing(itemToTransform, key));
114116
return WriteModification.builder()
115117
.transformedItem(Collections.unmodifiableMap(itemToTransform))
116118
.build();
117119
}
118120

119-
private void insertUuidInItemToTransform(Map<String, AttributeValue> itemToTransform,
120-
String key) {
121-
itemToTransform.put(key, AttributeValue.builder().s(UUID.randomUUID().toString()).build());
121+
private static void insertUuidIfMissing(Map<String, AttributeValue> item, String key) {
122+
AttributeValue existing = item.get(key);
123+
boolean missing = existing == null || StringUtils.isEmpty(existing.s());
124+
if (missing) {
125+
item.put(key, AttributeValue.builder().s(UUID.randomUUID().toString()).build());
126+
}
122127
}
123128

124129
public static final class AttributeTags {

services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtensionTest.java

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,11 @@ public class AutoGeneratedUuidExtensionTest {
6666
.build();
6767

6868
@Test
69-
public void beforeWrite_updateItemOperation_hasUuidInItem_doesNotCreateUpdateExpressionAndFilters() {
69+
public void beforeWrite_withExistingUuid_doesNotOverwriteValue() {
7070
ItemWithUuid SimpleItem = new ItemWithUuid();
7171
SimpleItem.setId(RECORD_ID);
72-
String uuidAttribute = String.valueOf(UUID.randomUUID());
73-
SimpleItem.setUuidAttribute(uuidAttribute);
72+
String initialUuid = String.valueOf(UUID.randomUUID());
73+
SimpleItem.setUuidAttribute(initialUuid);
7474

7575
Map<String, AttributeValue> items = ITEM_WITH_UUID_MAPPER.itemToMap(SimpleItem, true);
7676
assertThat(items).hasSize(2);
@@ -85,13 +85,15 @@ public void beforeWrite_updateItemOperation_hasUuidInItem_doesNotCreateUpdateExp
8585
Map<String, AttributeValue> transformedItem = result.transformedItem();
8686
assertThat(transformedItem).isNotNull().hasSize(2);
8787
assertThat(transformedItem).containsEntry("id", AttributeValue.fromS(RECORD_ID));
88-
isValidUuid(transformedItem.get("uuidAttribute").s());
8988
assertThat(result.updateExpression()).isNull();
9089

90+
String uuidValue = result.transformedItem().get("uuidAttribute").s();
91+
isValidUuid(uuidValue);
92+
assertThat(uuidValue).isEqualTo(initialUuid);
9193
}
9294

9395
@Test
94-
public void beforeWrite_updateItemOperation_hasNoUuidInItem_doesNotCreatesUpdateExpressionAndFilters() {
96+
public void beforeWrite_withMissingUuid_generatesNewValue() {
9597
ItemWithUuid SimpleItem = new ItemWithUuid();
9698
SimpleItem.setId(RECORD_ID);
9799

@@ -108,8 +110,31 @@ public void beforeWrite_updateItemOperation_hasNoUuidInItem_doesNotCreatesUpdate
108110
Map<String, AttributeValue> transformedItem = result.transformedItem();
109111
assertThat(transformedItem).isNotNull().hasSize(2);
110112
assertThat(transformedItem).containsEntry("id", AttributeValue.fromS(RECORD_ID));
111-
isValidUuid(transformedItem.get("uuidAttribute").s());
112113
assertThat(result.updateExpression()).isNull();
114+
115+
isValidUuid(result.transformedItem().get("uuidAttribute").s());
116+
assertThat(items).doesNotContainKey("uuidAttribute");
117+
}
118+
119+
@Test
120+
public void beforeWrite_withEmptyUuid_generatesNewValue() {
121+
ItemWithUuid item = new ItemWithUuid();
122+
item.setId(RECORD_ID);
123+
item.setUuidAttribute(""); // empty should be treated as missing
124+
125+
Map<String, AttributeValue> items = ITEM_WITH_UUID_MAPPER.itemToMap(item, true);
126+
127+
WriteModification result = atomicCounterExtension.beforeWrite(
128+
DefaultDynamoDbExtensionContext.builder()
129+
.items(items)
130+
.tableMetadata(ITEM_WITH_UUID_MAPPER.tableMetadata())
131+
.operationName(OperationName.UPDATE_ITEM)
132+
.operationContext(PRIMARY_CONTEXT)
133+
.build());
134+
135+
assertThat(result.updateExpression()).isNull();
136+
String uuidValue = result.transformedItem().get("uuidAttribute").s();
137+
isValidUuid(uuidValue);
113138
}
114139

115140
@Test

0 commit comments

Comments
 (0)