Skip to content

Commit 7cec4a2

Browse files
committed
Added support for DynamoDbAutoGeneratedKey annotation
1 parent 7a78bfa commit 7cec4a2

File tree

11 files changed

+1509
-5
lines changed

11 files changed

+1509
-5
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,219 @@
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.HashSet;
22+
import java.util.Map;
23+
import java.util.Objects;
24+
import java.util.Set;
25+
import java.util.UUID;
26+
import java.util.function.Consumer;
27+
import software.amazon.awssdk.annotations.SdkPublicApi;
28+
import software.amazon.awssdk.annotations.ThreadSafe;
29+
import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType;
30+
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClientExtension;
31+
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbExtensionContext;
32+
import software.amazon.awssdk.enhanced.dynamodb.EnhancedType;
33+
import software.amazon.awssdk.enhanced.dynamodb.IndexMetadata;
34+
import software.amazon.awssdk.enhanced.dynamodb.TableMetadata;
35+
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTag;
36+
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableMetadata;
37+
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
38+
import software.amazon.awssdk.utils.StringUtils;
39+
import software.amazon.awssdk.utils.Validate;
40+
41+
/**
42+
* Generates a random UUID (via {@link java.util.UUID#randomUUID()}) for any attribute tagged with
43+
* {@code @DynamoDbAutoGeneratedKey} when that attribute is missing or empty on a write (put/update).
44+
* <p>
45+
* <b>Key Difference from @DynamoDbAutoGeneratedUuid:</b> This extension only generates UUIDs when the
46+
* attribute value is null or empty, preserving existing values. In contrast, {@code @DynamoDbAutoGeneratedUuid} always generates
47+
* new UUIDs regardless of existing values.
48+
* <p>
49+
* <b>Conflict Detection:</b> This extension cannot be used together with {@code @DynamoDbAutoGeneratedUuid} on the same
50+
* attribute. If both annotations are applied to the same field, an {@link IllegalArgumentException} will be thrown at runtime to
51+
* prevent unpredictable behavior based on extension load order.
52+
* <p>
53+
* The annotation may be placed <b>only</b> on key attributes:
54+
* <ul>
55+
* <li>Primary partition key (PK) or primary sort key (SK)</li>
56+
* <li>Partition key or sort key of any secondary index (GSI or LSI)</li>
57+
* </ul>
58+
*
59+
* <p><b>Validation:</b> The extension enforces this at runtime during {@link #beforeWrite} by comparing the
60+
* annotated attributes against the table's known key attributes. If an annotated attribute
61+
* is not a PK/SK or an GSI/LSI, an {@link IllegalArgumentException} is thrown.</p>
62+
*
63+
* <p><b>UpdateBehavior Limitations:</b> {@code @DynamoDbUpdateBehavior} has no effect on primary keys due to
64+
* DynamoDB's UpdateItem API requirements. It only affects secondary index keys.</p>
65+
*/
66+
@SdkPublicApi
67+
@ThreadSafe
68+
public final class AutoGeneratedKeyExtension implements DynamoDbEnhancedClientExtension {
69+
70+
/**
71+
* Custom metadata key under which we store the set of annotated attribute names.
72+
*/
73+
private static final String CUSTOM_METADATA_KEY =
74+
"software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedKeyExtension:AutoGeneratedKeyAttribute";
75+
76+
/**
77+
* Metadata key used by AutoGeneratedUuidExtension to detect conflicts.
78+
*/
79+
private static final String UUID_EXTENSION_METADATA_KEY =
80+
"software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension:AutoGeneratedUuidAttribute";
81+
82+
private static final AutoGeneratedKeyAttribute AUTO_GENERATED_KEY_ATTRIBUTE = new AutoGeneratedKeyAttribute();
83+
84+
private AutoGeneratedKeyExtension() {
85+
}
86+
87+
public static Builder builder() {
88+
return new Builder();
89+
}
90+
91+
/**
92+
* If this table has attributes tagged for auto-generation, insert a UUID value into the outgoing item for any such attribute
93+
* that is currently missing/empty. Unlike {@code @DynamoDbAutoGeneratedUuid}, this preserves existing values.
94+
* <p>
95+
* Also validates that the annotation is only used on PK/SK/GSI/LSI key attributes and that there are no conflicts with
96+
*
97+
* @DynamoDbAutoGeneratedUuid.
98+
*/
99+
@Override
100+
public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite context) {
101+
Collection<String> taggedAttributes = context.tableMetadata()
102+
.customMetadataObject(CUSTOM_METADATA_KEY, Collection.class)
103+
.orElse(null);
104+
105+
if (taggedAttributes == null || taggedAttributes.isEmpty()) {
106+
return WriteModification.builder().build();
107+
}
108+
109+
// Check for conflicts with @DynamoDbAutoGeneratedUuid
110+
Collection<String> uuidTaggedAttributes = context.tableMetadata()
111+
.customMetadataObject(UUID_EXTENSION_METADATA_KEY, Collection.class)
112+
.orElse(Collections.emptyList());
113+
114+
taggedAttributes.stream()
115+
.filter(uuidTaggedAttributes::contains)
116+
.findFirst()
117+
.ifPresent(attribute -> {
118+
throw new IllegalArgumentException(
119+
"Attribute '" + attribute + "' cannot have both @DynamoDbAutoGeneratedKey and "
120+
+ "@DynamoDbAutoGeneratedUuid annotations. These annotations have conflicting behaviors "
121+
+ "and cannot be used together on the same attribute.");
122+
});
123+
124+
TableMetadata meta = context.tableMetadata();
125+
Set<String> allowedKeys = new HashSet<>();
126+
127+
// ensure every @DynamoDbAutoGeneratedKey attribute is a PK/SK or GSI/LSI. If not, throw IllegalArgumentException
128+
allowedKeys.add(meta.primaryPartitionKey());
129+
meta.primarySortKey().ifPresent(allowedKeys::add);
130+
131+
for (IndexMetadata idx : meta.indices()) {
132+
String indexName = idx.name();
133+
allowedKeys.add(meta.indexPartitionKey(indexName));
134+
meta.indexSortKey(indexName).ifPresent(allowedKeys::add);
135+
}
136+
137+
taggedAttributes.stream()
138+
.filter(attr -> !allowedKeys.contains(attr))
139+
.findFirst()
140+
.ifPresent(attr -> {
141+
throw new IllegalArgumentException(
142+
"@DynamoDbAutoGeneratedKey can only be applied to key attributes: "
143+
+ "primary partition key, primary sort key, or GSI/LSI partition/sort keys."
144+
+ "Invalid placement on attribute: " + attr);
145+
});
146+
147+
// Generate UUIDs for missing/empty annotated attributes
148+
Map<String, AttributeValue> itemToTransform = new HashMap<>(context.items());
149+
taggedAttributes.forEach(attr -> insertUuidIfMissing(itemToTransform, attr));
150+
151+
return WriteModification.builder()
152+
.transformedItem(Collections.unmodifiableMap(itemToTransform))
153+
.build();
154+
}
155+
156+
private void insertUuidIfMissing(Map<String, AttributeValue> itemToTransform, String key) {
157+
AttributeValue existing = itemToTransform.get(key);
158+
if (Objects.isNull(existing) || StringUtils.isBlank(existing.s())) {
159+
itemToTransform.put(key, AttributeValue.builder().s(UUID.randomUUID().toString()).build());
160+
}
161+
}
162+
163+
/**
164+
* Static helpers used by the {@code @BeanTableSchemaAttributeTag}-based annotation tag.
165+
*/
166+
public static final class AttributeTags {
167+
private AttributeTags() {
168+
}
169+
170+
/**
171+
* @return a {@link StaticAttributeTag} that marks the attribute for auto-generated key behavior.
172+
*/
173+
public static StaticAttributeTag autoGeneratedKeyAttribute() {
174+
return AUTO_GENERATED_KEY_ATTRIBUTE;
175+
}
176+
}
177+
178+
/**
179+
* Stateless builder.
180+
*/
181+
public static final class Builder {
182+
private Builder() {
183+
}
184+
185+
public AutoGeneratedKeyExtension build() {
186+
return new AutoGeneratedKeyExtension();
187+
}
188+
}
189+
190+
/**
191+
* Validates Java type and records the tagged attribute into table metadata so {@link #beforeWrite} can find it at runtime.
192+
*/
193+
private static final class AutoGeneratedKeyAttribute implements StaticAttributeTag {
194+
195+
@Override
196+
public <R> void validateType(String attributeName,
197+
EnhancedType<R> type,
198+
AttributeValueType attributeValueType) {
199+
200+
Validate.notNull(type, "type is null");
201+
Validate.notNull(type.rawClass(), "rawClass is null");
202+
Validate.notNull(attributeValueType, "attributeValueType is null");
203+
204+
if (!type.rawClass().equals(String.class)) {
205+
throw new IllegalArgumentException(String.format(
206+
"Attribute '%s' of Class type %s is not a suitable Java Class type to be used as a Auto Generated "
207+
+ "Key attribute. Only String Class type is supported.", attributeName, type.rawClass()));
208+
}
209+
}
210+
211+
@Override
212+
public Consumer<StaticTableMetadata.Builder> modifyMetadata(String attributeName,
213+
AttributeValueType attributeValueType) {
214+
// Record the names of the attributes annotated with @DynamoDbAutoGeneratedKey for later lookup in beforeWrite()
215+
return metadata -> metadata.addCustomMetadataObject(
216+
CUSTOM_METADATA_KEY, Collections.singleton(attributeName));
217+
}
218+
}
219+
}

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@
3939
* every time a new record is written to the database. The generated UUID is obtained using the
4040
* {@link java.util.UUID#randomUUID()} method.
4141
* <p>
42+
* <b>Key Difference from @DynamoDbAutoGeneratedKey:</b> This extension always generates new UUIDs on every write,
43+
* regardless of existing values. In contrast, {@code @DynamoDbAutoGeneratedKey} only generates UUIDs when the
44+
* attribute value is null or empty, preserving existing values.
45+
* <p>
46+
* <b>Conflict Detection:</b> This extension cannot be used together with {@code @DynamoDbAutoGeneratedKey} on the same
47+
* attribute. If both annotations are applied to the same field, an {@link IllegalArgumentException} will be thrown
48+
* at runtime to prevent unpredictable behavior.
49+
* <p>
4250
* This extension is not loaded by default when you instantiate a
4351
* {@link software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient}. Therefore, you need to specify it in a custom
4452
* extension when creating the enhanced client.
@@ -79,6 +87,13 @@
7987
public final class AutoGeneratedUuidExtension implements DynamoDbEnhancedClientExtension {
8088
private static final String CUSTOM_METADATA_KEY =
8189
"software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension:AutoGeneratedUuidAttribute";
90+
91+
/**
92+
* Metadata key used by AutoGeneratedKeyExtension to detect conflicts.
93+
*/
94+
private static final String KEY_EXTENSION_METADATA_KEY =
95+
"software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedKeyExtension:AutoGeneratedKeyAttribute";
96+
8297
private static final AutoGeneratedUuidAttribute AUTO_GENERATED_UUID_ATTRIBUTE = new AutoGeneratedUuidAttribute();
8398

8499
private AutoGeneratedUuidExtension() {
@@ -109,6 +124,21 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex
109124
return WriteModification.builder().build();
110125
}
111126

127+
// Check for conflicts with @DynamoDbAutoGeneratedKey
128+
Collection<String> keyTaggedAttributes = context.tableMetadata()
129+
.customMetadataObject(KEY_EXTENSION_METADATA_KEY, Collection.class)
130+
.orElse(Collections.emptyList());
131+
132+
customMetadataObject.stream()
133+
.filter(keyTaggedAttributes::contains)
134+
.findFirst()
135+
.ifPresent(attribute -> {
136+
throw new IllegalArgumentException(
137+
"Attribute '" + attribute + "' cannot have both @DynamoDbAutoGeneratedKey and "
138+
+ "@DynamoDbAutoGeneratedUuid annotations. These annotations have conflicting behaviors "
139+
+ "and cannot be used together on the same attribute.");
140+
});
141+
112142
Map<String, AttributeValue> itemToTransform = new HashMap<>(context.items());
113143
customMetadataObject.forEach(key -> insertUuidInItemToTransform(itemToTransform, key));
114144
return WriteModification.builder()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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.UpdateBehavior;
26+
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.BeanTableSchemaAttributeTag;
27+
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbUpdateBehavior;
28+
29+
/**
30+
* Annotation that marks a key attribute to be automatically populated with a random UUID if no value is provided during a write
31+
* operation (put or update). This annotation is intended to work specifically with key attributes.
32+
*
33+
* <p>This annotation is designed for use with the V2 {@link software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema}.
34+
* It is registered via {@link BeanTableSchemaAttributeTag} and its behavior is implemented by
35+
* {@link software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedKeyExtension}.</p>
36+
*
37+
* <h3>Where this annotation can be applied</h3>
38+
* This annotation is only valid on attributes that serve as keys:
39+
* <ul>
40+
* <li>The table's primary partition key or sort key</li>
41+
* <li>The partition key or sort key of a secondary index (GSI or LSI)</li>
42+
* </ul>
43+
* If applied to any other attribute, the {@code AutoGeneratedKeyExtension} will throw an
44+
* {@link IllegalArgumentException} at runtime.
45+
*
46+
* <h3>How values are generated</h3>
47+
* <ul>
48+
* <li>On writes where the annotated attribute is null or empty, a new UUID value is generated
49+
* using {@link java.util.UUID#randomUUID()}.</li>
50+
* <li>If a value is already set on the attribute, that value is preserved and not replaced.</li>
51+
* <li>This behavior differs from {@code @DynamoDbAutoGeneratedUuid}, which always generates new UUIDs regardless of existing
52+
* values.</li>
53+
* </ul>
54+
*
55+
* <h3>Behavior with UpdateBehavior</h3>
56+
* <p><strong>Primary Keys:</strong> {@link DynamoDbUpdateBehavior} has <strong>no effect</strong> on primary partition keys
57+
* or primary sort keys. Primary keys are required for UpdateItem operations in DynamoDB and cannot be conditionally
58+
* updated. UUIDs will be generated whenever the primary key attribute is missing or empty, regardless of any
59+
* {@code UpdateBehavior} setting.</p>
60+
*
61+
* <p><strong>Secondary Index Keys:</strong> For GSI/LSI keys, {@link DynamoDbUpdateBehavior} can be used:
62+
* <ul>
63+
* <li>{@link UpdateBehavior#WRITE_ALWAYS} (default) – Generate a new UUID whenever the attribute is missing during write.</li>
64+
* <li>{@link UpdateBehavior#WRITE_IF_NOT_EXISTS} – Generate a UUID only on the first write, preserving the value on
65+
* subsequent updates.</li>
66+
* </ul></p>
67+
*
68+
* <h3>Type restriction</h3>
69+
* This annotation is only valid on attributes of type {@link String}.
70+
*/
71+
@SdkPublicApi
72+
@Documented
73+
@Retention(RetentionPolicy.RUNTIME)
74+
@Target( {ElementType.METHOD, ElementType.FIELD})
75+
@BeanTableSchemaAttributeTag(AutoGeneratedKeyTag.class)
76+
public @interface DynamoDbAutoGeneratedKey {
77+
}
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+
}

0 commit comments

Comments
 (0)