Skip to content

Commit fa35bfc

Browse files
committed
Added support for DynamoDbAutoGeneratedKey annotation [Fix for #5497 - ConditionalCheckFailedException with version attribute and partition key using auto-generated UUID]
1 parent c497318 commit fa35bfc

File tree

7 files changed

+1288
-0
lines changed

7 files changed

+1288
-0
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "feature",
3+
"category": "Amazon DynamoDB Enhanced Client",
4+
"contributor": "",
5+
"description": "Added the support for DynamoDbAutoGeneratedKey annotation"
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.enhanced.dynamodb.extensions;
17+
18+
import java.util.Collection;
19+
import java.util.Collections;
20+
import java.util.HashMap;
21+
import java.util.Map;
22+
import java.util.UUID;
23+
import java.util.function.Consumer;
24+
import software.amazon.awssdk.annotations.SdkPublicApi;
25+
import software.amazon.awssdk.annotations.ThreadSafe;
26+
import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType;
27+
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClientExtension;
28+
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbExtensionContext;
29+
import software.amazon.awssdk.enhanced.dynamodb.EnhancedType;
30+
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTag;
31+
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableMetadata;
32+
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
33+
import software.amazon.awssdk.utils.Validate;
34+
35+
/**
36+
* This extension facilitates the automatic generation of a unique UUID for any attribute tagged with
37+
* {@code @DynamoDbAutoGeneratedKey}. The generated value uses {@link UUID#randomUUID()}.
38+
* <p>
39+
* Register this extension explicitly when building the {@link software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient}.
40+
* <p>
41+
* Example:
42+
* <pre>
43+
* DynamoDbEnhancedClient client = DynamoDbEnhancedClient.builder()
44+
* .dynamoDbClient(lowLevelClient)
45+
* .extensions(AutoGeneratedKeyExtension.create())
46+
* .build();
47+
* </pre>
48+
*/
49+
@SdkPublicApi
50+
@ThreadSafe
51+
public final class AutoGeneratedKeyExtension implements DynamoDbEnhancedClientExtension {
52+
53+
private static final String CUSTOM_METADATA_KEY =
54+
"software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedKeyExtension:AutoGeneratedKeyAttribute";
55+
56+
private static final AutoGeneratedKeyAttribute AUTO_GENERATED_KEY_ATTRIBUTE = new AutoGeneratedKeyAttribute();
57+
58+
private AutoGeneratedKeyExtension() {
59+
}
60+
61+
/**
62+
* @return an instance of {@link AutoGeneratedKeyExtension}
63+
*/
64+
public static AutoGeneratedKeyExtension create() {
65+
return new AutoGeneratedKeyExtension();
66+
}
67+
68+
/**
69+
* If this table has attributes tagged for auto-generation, insert a UUID value into the outgoing item for any such attribute
70+
* that is currently missing/empty.
71+
*/
72+
@Override
73+
public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite context) {
74+
Collection<String> taggedAttributes =
75+
context.tableMetadata()
76+
.customMetadataObject(CUSTOM_METADATA_KEY, Collection.class)
77+
.orElse(null);
78+
79+
if (taggedAttributes == null || taggedAttributes.isEmpty()) {
80+
return WriteModification.builder().build();
81+
}
82+
83+
Map<String, AttributeValue> itemToTransform = new HashMap<>(context.items());
84+
taggedAttributes.forEach(attr -> insertUuidIfMissing(itemToTransform, attr));
85+
86+
return WriteModification.builder()
87+
.transformedItem(Collections.unmodifiableMap(itemToTransform))
88+
.build();
89+
}
90+
91+
private void insertUuidIfMissing(Map<String, AttributeValue> itemToTransform, String key) {
92+
AttributeValue existing = itemToTransform.get(key);
93+
boolean missing = existing == null || existing.s() == null || existing.s().isEmpty();
94+
if (missing) {
95+
itemToTransform.put(key, AttributeValue.builder().s(UUID.randomUUID().toString()).build());
96+
}
97+
}
98+
99+
/**
100+
* Static helpers used by the {@code @BeanTableSchemaAttributeTag}-based annotation tag.
101+
*/
102+
public static final class AttributeTags {
103+
private AttributeTags() {
104+
}
105+
106+
/**
107+
* @return a {@link StaticAttributeTag} that marks the attribute for auto-generated key behavior.
108+
*/
109+
public static StaticAttributeTag autoGeneratedKeyAttribute() {
110+
return AUTO_GENERATED_KEY_ATTRIBUTE;
111+
}
112+
}
113+
114+
/**
115+
* Validates the Java type and writes table metadata so {@link #beforeWrite} can find the tagged attributes at runtime.
116+
*/
117+
private static final class AutoGeneratedKeyAttribute implements StaticAttributeTag {
118+
119+
@Override
120+
public <R> void validateType(String attributeName,
121+
EnhancedType<R> type,
122+
AttributeValueType attributeValueType) {
123+
124+
Validate.notNull(type, "type is null");
125+
Validate.notNull(type.rawClass(), "rawClass is null");
126+
Validate.notNull(attributeValueType, "attributeValueType is null");
127+
128+
if (!String.class.equals(type.rawClass())) {
129+
throw new IllegalArgumentException(String.format(
130+
"Attribute '%s' of Java type %s is not valid for @DynamoDbAutoGeneratedKey. Only String is supported.",
131+
attributeName, type.rawClass()));
132+
}
133+
}
134+
135+
@Override
136+
public Consumer<StaticTableMetadata.Builder> modifyMetadata(String attributeName,
137+
AttributeValueType attributeValueType) {
138+
return metadata -> metadata
139+
.addCustomMetadataObject(CUSTOM_METADATA_KEY, Collections.singleton(attributeName))
140+
.markAttributeAsKey(attributeName, attributeValueType);
141+
}
142+
}
143+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.enhanced.dynamodb.extensions.annotations;
17+
18+
import java.lang.annotation.Documented;
19+
import java.lang.annotation.ElementType;
20+
import java.lang.annotation.Retention;
21+
import java.lang.annotation.RetentionPolicy;
22+
import java.lang.annotation.Target;
23+
import software.amazon.awssdk.annotations.SdkPublicApi;
24+
import software.amazon.awssdk.enhanced.dynamodb.internal.extensions.AutoGeneratedKeyTag;
25+
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.BeanTableSchemaAttributeTag;
26+
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbUpdateBehavior;
27+
28+
/**
29+
* Annotation that marks a DynamoDB attribute as an auto-generated key.
30+
*
31+
* <p>
32+
* When applied, the associated attribute will be automatically populated with a randomly generated {@link java.util.UUID} string
33+
* value if it is not explicitly set when performing a {@code putItem} or {@code updateItem} operation. The exact behavior across
34+
* subsequent updates depends on the update policy specified with {@link DynamoDbUpdateBehavior}:
35+
* </p>
36+
*
37+
* <ul>
38+
* <li>
39+
* <b>Default (WRITE_ALWAYS):</b> The attribute is regenerated on every write if it is missing,
40+
* meaning a new UUID may be produced on each update. This is useful for fields like
41+
* <i>lastUpdatedKey</i> that are expected to change frequently.
42+
* </li>
43+
* <li>
44+
* <b>WRITE_IF_NOT_EXISTS:</b> The attribute is generated only once (on the initial insert) and
45+
* preserved on subsequent updates. This is the typical choice for attributes such as
46+
* <i>createdKey</i> that should remain stable throughout the lifecycle of an item.
47+
* </li>
48+
* </ul>
49+
*
50+
* <p>
51+
* Typical usage is to apply this annotation to a partition key or sort key property of type
52+
* {@link String}. The annotated element can be either a getter method or a class field.
53+
* </p>
54+
*
55+
* <p>
56+
* This annotation is functionally similar to the legacy V1 {@code @DynamoDBAutoGeneratedKey} and
57+
* is intended for use with the V2 {@link software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema}.
58+
* </p>
59+
*
60+
* @see java.util.UUID
61+
* @see software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema
62+
* @see software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbUpdateBehavior
63+
*/
64+
@SdkPublicApi
65+
@Documented
66+
@Retention(RetentionPolicy.RUNTIME)
67+
@Target( {ElementType.METHOD, ElementType.FIELD})
68+
@BeanTableSchemaAttributeTag(AutoGeneratedKeyTag.class)
69+
public @interface DynamoDbAutoGeneratedKey {
70+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.enhanced.dynamodb.internal.extensions;
17+
18+
import software.amazon.awssdk.annotations.SdkInternalApi;
19+
import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedKeyExtension;
20+
import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedKey;
21+
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTag;
22+
23+
@SdkInternalApi
24+
public final class AutoGeneratedKeyTag {
25+
26+
private AutoGeneratedKeyTag() {
27+
}
28+
29+
public static StaticAttributeTag attributeTagFor(DynamoDbAutoGeneratedKey annotation) {
30+
return AutoGeneratedKeyExtension.AttributeTags.autoGeneratedKeyAttribute();
31+
}
32+
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.enhanced.dynamodb;
17+
18+
import java.util.UUID;
19+
20+
public class UuidTestUtils {
21+
22+
public static boolean isValidUuid(String uuid) {
23+
try {
24+
UUID.fromString(uuid);
25+
return true;
26+
} catch (Exception e) {
27+
return false;
28+
}
29+
}
30+
}

0 commit comments

Comments
 (0)