Skip to content

Commit a1f9cfd

Browse files
committed
Optimistic Locking for Delete Operations
1 parent 837715e commit a1f9cfd

File tree

14 files changed

+1614
-8
lines changed

14 files changed

+1614
-8
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,10 @@ default CompletableFuture<T> deleteItem(T keyItem) {
247247
throw new UnsupportedOperationException();
248248
}
249249

250+
default CompletableFuture<T> deleteItem(T keyItem, boolean useOptimisticLocking) {
251+
throw new UnsupportedOperationException();
252+
}
253+
250254
/**
251255
* Deletes a single item from the mapped table using a supplied primary {@link Key}.
252256
* <p>

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,10 @@ default T deleteItem(T keyItem) {
245245
throw new UnsupportedOperationException();
246246
}
247247

248+
default T deleteItem(T keyItem, boolean useOptimisticLocking) {
249+
throw new UnsupportedOperationException();
250+
}
251+
248252
/**
249253
* Deletes a single item from the mapped table using a supplied primary {@link Key}.
250254
* <p>

services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbAsyncTable.java

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package software.amazon.awssdk.enhanced.dynamodb.internal.client;
1717

1818
import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.createKeyFromItem;
19+
import static software.amazon.awssdk.enhanced.dynamodb.model.OptimisticLockingHelper.conditionallyApplyOptimisticLocking;
1920

2021
import java.util.ArrayList;
2122
import java.util.concurrent.CompletableFuture;
@@ -124,28 +125,55 @@ public CompletableFuture<Void> createTable() {
124125
.build());
125126
}
126127

128+
/**
129+
* Supports optimistic locking via {@link software.amazon.awssdk.enhanced.dynamodb.model.OptimisticLockingHelper}.
130+
*/
127131
@Override
128132
public CompletableFuture<T> deleteItem(DeleteItemEnhancedRequest request) {
129133
TableOperation<T, ?, ?, DeleteItemEnhancedResponse<T>> operation = DeleteItemOperation.create(request);
130134
return operation.executeOnPrimaryIndexAsync(tableSchema, tableName, extension, dynamoDbClient)
131135
.thenApply(DeleteItemEnhancedResponse::attributes);
132136
}
133137

138+
/**
139+
* Supports optimistic locking via {@link software.amazon.awssdk.enhanced.dynamodb.model.OptimisticLockingHelper}.
140+
*/
134141
@Override
135142
public CompletableFuture<T> deleteItem(Consumer<DeleteItemEnhancedRequest.Builder> requestConsumer) {
136143
DeleteItemEnhancedRequest.Builder builder = DeleteItemEnhancedRequest.builder();
137144
requestConsumer.accept(builder);
138145
return deleteItem(builder.build());
139146
}
140147

148+
/**
149+
* Does not support optimistic locking. Use {@link #deleteItem(Object, boolean)} for optimistic locking support.
150+
*/
141151
@Override
142152
public CompletableFuture<T> deleteItem(Key key) {
143153
return deleteItem(r -> r.key(key));
144154
}
145155

156+
/**
157+
* @deprecated Use {@link #deleteItem(Object, boolean)} instead to explicitly control optimistic locking behavior.
158+
*/
146159
@Override
160+
@Deprecated
147161
public CompletableFuture<T> deleteItem(T keyItem) {
148-
return deleteItem(keyFrom(keyItem));
162+
return deleteItem(keyItem, false);
163+
}
164+
165+
/**
166+
* Deletes an item from the table with optional optimistic locking.
167+
*
168+
* @param keyItem the item containing the key to delete
169+
* @param useOptimisticLocking if true, applies optimistic locking if the item has version information
170+
* @return a CompletableFuture containing the deleted item, or null if the item was not found
171+
*/
172+
@Override
173+
public CompletableFuture<T> deleteItem(T keyItem, boolean useOptimisticLocking) {
174+
DeleteItemEnhancedRequest request = DeleteItemEnhancedRequest.builder().key(keyFrom(keyItem)).build();
175+
request = conditionallyApplyOptimisticLocking(request, keyItem, tableSchema, useOptimisticLocking);
176+
return deleteItem(request);
149177
}
150178

151179
@Override
@@ -311,7 +339,7 @@ public CompletableFuture<T> updateItem(T item) {
311339
public Key keyFrom(T item) {
312340
return createKeyFromItem(item, tableSchema, TableMetadata.primaryIndexName());
313341
}
314-
342+
315343

316344
@Override
317345
public CompletableFuture<Void> deleteTable() {

services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbTable.java

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package software.amazon.awssdk.enhanced.dynamodb.internal.client;
1717

1818
import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.createKeyFromItem;
19+
import static software.amazon.awssdk.enhanced.dynamodb.model.OptimisticLockingHelper.conditionallyApplyOptimisticLocking;
1920

2021
import java.util.ArrayList;
2122
import java.util.function.Consumer;
@@ -126,27 +127,54 @@ public void createTable() {
126127
.build());
127128
}
128129

130+
/**
131+
* Supports optimistic locking via {@link software.amazon.awssdk.enhanced.dynamodb.model.OptimisticLockingHelper}.
132+
*/
129133
@Override
130134
public T deleteItem(DeleteItemEnhancedRequest request) {
131135
TableOperation<T, ?, ?, DeleteItemEnhancedResponse<T>> operation = DeleteItemOperation.create(request);
132136
return operation.executeOnPrimaryIndex(tableSchema, tableName, extension, dynamoDbClient).attributes();
133137
}
134138

139+
/**
140+
* Supports optimistic locking via {@link software.amazon.awssdk.enhanced.dynamodb.model.OptimisticLockingHelper}.
141+
*/
135142
@Override
136143
public T deleteItem(Consumer<DeleteItemEnhancedRequest.Builder> requestConsumer) {
137144
DeleteItemEnhancedRequest.Builder builder = DeleteItemEnhancedRequest.builder();
138145
requestConsumer.accept(builder);
139146
return deleteItem(builder.build());
140147
}
141148

149+
/**
150+
* Does not support optimistic locking. Use {@link #deleteItem(Object, boolean)} for optimistic locking support.
151+
*/
142152
@Override
143153
public T deleteItem(Key key) {
144154
return deleteItem(r -> r.key(key));
145155
}
146156

157+
/**
158+
* @deprecated Use {@link #deleteItem(Object, boolean)} instead to explicitly control optimistic locking behavior.
159+
*/
147160
@Override
161+
@Deprecated
148162
public T deleteItem(T keyItem) {
149-
return deleteItem(keyFrom(keyItem));
163+
return deleteItem(keyItem, false);
164+
}
165+
166+
/**
167+
* Deletes an item from the table with optional optimistic locking.
168+
*
169+
* @param keyItem the item containing the key to delete
170+
* @param useOptimisticLocking if true, applies optimistic locking if the item has version information
171+
* @return the deleted item, or null if the item was not found
172+
*/
173+
@Override
174+
public T deleteItem(T keyItem, boolean useOptimisticLocking) {
175+
DeleteItemEnhancedRequest request = DeleteItemEnhancedRequest.builder().key(keyFrom(keyItem)).build();
176+
request = conditionallyApplyOptimisticLocking(request, keyItem, tableSchema, useOptimisticLocking);
177+
return deleteItem(request);
150178
}
151179

152180
@Override

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
package software.amazon.awssdk.enhanced.dynamodb.model;
1717

18+
import static software.amazon.awssdk.enhanced.dynamodb.model.OptimisticLockingHelper.createVersionCondition;
19+
1820
import java.util.Objects;
1921
import java.util.function.Consumer;
2022
import software.amazon.awssdk.annotations.NotThreadSafe;
@@ -24,6 +26,7 @@
2426
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable;
2527
import software.amazon.awssdk.enhanced.dynamodb.Expression;
2628
import software.amazon.awssdk.enhanced.dynamodb.Key;
29+
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
2730
import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest;
2831
import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
2932
import software.amazon.awssdk.services.dynamodb.model.ReturnConsumedCapacity;
@@ -289,6 +292,22 @@ public Builder returnValuesOnConditionCheckFailure(String returnValuesOnConditio
289292
return this;
290293
}
291294

295+
/**
296+
* Adds optimistic locking to this delete request.
297+
* <p>
298+
* This method applies a condition expression that ensures the delete operation only succeeds
299+
* if the version attribute of the item matches the provided expected value.
300+
*
301+
* @param versionValue the expected version value that must match for the deletion to succeed
302+
* @param versionAttributeName the name of the version attribute in the DynamoDB table
303+
* @return a builder of this type with optimistic locking condition applied
304+
* @throws IllegalArgumentException if any parameter is null
305+
*/
306+
public Builder withOptimisticLocking(AttributeValue versionValue, String versionAttributeName) {
307+
Expression optimisticLockingCondition = createVersionCondition(versionValue, versionAttributeName);
308+
return conditionExpression(optimisticLockingCondition);
309+
}
310+
292311
public DeleteItemEnhancedRequest build() {
293312
return new DeleteItemEnhancedRequest(this);
294313
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
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.model;
17+
18+
import java.util.Optional;
19+
import software.amazon.awssdk.annotations.SdkPublicApi;
20+
import software.amazon.awssdk.enhanced.dynamodb.Expression;
21+
import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
22+
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
23+
24+
/**
25+
* Utility class for adding optimistic locking to DynamoDB delete operations.
26+
* <p>
27+
* Optimistic locking prevents concurrent modifications by checking that an item's version hasn't changed since it was last read.
28+
* If the version has changed, the delete operation fails with a {@code ConditionalCheckFailedException}.
29+
*/
30+
@SdkPublicApi
31+
public final class OptimisticLockingHelper {
32+
33+
private OptimisticLockingHelper() {
34+
}
35+
36+
/**
37+
* Adds optimistic locking to a delete request.
38+
*
39+
* @param request the original delete request
40+
* @param versionValue the expected version value
41+
* @param versionAttributeName the version attribute name
42+
* @return delete request with optimistic locking condition
43+
*/
44+
public static DeleteItemEnhancedRequest withOptimisticLocking(
45+
DeleteItemEnhancedRequest request, AttributeValue versionValue, String versionAttributeName) {
46+
47+
Expression conditionExpression = createVersionCondition(versionValue, versionAttributeName);
48+
return request.toBuilder()
49+
.conditionExpression(conditionExpression)
50+
.build();
51+
}
52+
53+
/**
54+
* Adds optimistic locking to a transactional delete request.
55+
*
56+
* @param request the original transactional delete request
57+
* @param versionValue the expected version value
58+
* @param versionAttributeName the version attribute name
59+
* @return transactional delete request with optimistic locking condition
60+
*/
61+
public static TransactDeleteItemEnhancedRequest withOptimisticLocking(
62+
TransactDeleteItemEnhancedRequest request, AttributeValue versionValue, String versionAttributeName) {
63+
64+
Expression conditionExpression = createVersionCondition(versionValue, versionAttributeName);
65+
return request.toBuilder()
66+
.conditionExpression(conditionExpression)
67+
.build();
68+
}
69+
70+
/**
71+
* Conditionally applies optimistic locking if enabled and version information exists.
72+
*
73+
* @param <T> the type of the item
74+
* @param request the original delete request
75+
* @param keyItem the item containing version information
76+
* @param tableSchema the table schema
77+
* @param useOptimisticLocking if true, applies optimistic locking
78+
* @return delete request with optimistic locking if enabled and version exists, otherwise original request
79+
*/
80+
public static <T> DeleteItemEnhancedRequest conditionallyApplyOptimisticLocking(
81+
DeleteItemEnhancedRequest request, T keyItem, TableSchema<T> tableSchema, boolean useOptimisticLocking) {
82+
83+
if (!useOptimisticLocking) {
84+
return request;
85+
}
86+
87+
return getVersionAttributeName(tableSchema)
88+
.map(versionAttributeName -> {
89+
AttributeValue version = tableSchema.attributeValue(keyItem, versionAttributeName);
90+
return version != null ? withOptimisticLocking(request, version, versionAttributeName) : request;
91+
})
92+
.orElse(request);
93+
}
94+
95+
/**
96+
* Conditionally applies optimistic locking if enabled and version information exists.
97+
*
98+
* @param <T> the type of the item
99+
* @param request the original transactional delete request
100+
* @param keyItem the item containing version information
101+
* @param tableSchema the table schema
102+
* @param useOptimisticLocking if true, applies optimistic locking
103+
* @return delete request with optimistic locking if enabled and version exists, otherwise original request
104+
*/
105+
public static <T> TransactDeleteItemEnhancedRequest conditionallyApplyOptimisticLocking(
106+
TransactDeleteItemEnhancedRequest request, T keyItem, TableSchema<T> tableSchema, boolean useOptimisticLocking) {
107+
108+
if (!useOptimisticLocking) {
109+
return request;
110+
}
111+
112+
return getVersionAttributeName(tableSchema)
113+
.map(versionAttributeName -> {
114+
AttributeValue version = tableSchema.attributeValue(keyItem, versionAttributeName);
115+
return version != null ? withOptimisticLocking(request, version, versionAttributeName) : request;
116+
})
117+
.orElse(request);
118+
}
119+
120+
121+
/**
122+
* Creates a version condition expression.
123+
*
124+
* @param versionValue the expected version value
125+
* @param versionAttributeName the version attribute name
126+
* @return version check condition expression
127+
*/
128+
public static Expression createVersionCondition(AttributeValue versionValue, String versionAttributeName) {
129+
return Expression.builder()
130+
.expression(versionAttributeName + " = :version_value")
131+
.putExpressionValue(":version_value", versionValue)
132+
.build();
133+
}
134+
135+
/**
136+
* Gets the version attribute name from table schema.
137+
*
138+
* @param <T> the type of the item
139+
* @param tableSchema the table schema
140+
* @return version attribute name if present, empty otherwise
141+
*/
142+
public static <T> Optional<String> getVersionAttributeName(TableSchema<T> tableSchema) {
143+
return tableSchema.tableMetadata().customMetadataObject("VersionedRecordExtension:VersionAttribute", String.class);
144+
}
145+
}

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
package software.amazon.awssdk.enhanced.dynamodb.model;
1717

18+
import static software.amazon.awssdk.enhanced.dynamodb.model.OptimisticLockingHelper.createVersionCondition;
19+
1820
import java.util.Objects;
1921
import java.util.function.Consumer;
2022
import software.amazon.awssdk.annotations.NotThreadSafe;
@@ -24,6 +26,7 @@
2426
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient;
2527
import software.amazon.awssdk.enhanced.dynamodb.Expression;
2628
import software.amazon.awssdk.enhanced.dynamodb.Key;
29+
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
2730
import software.amazon.awssdk.services.dynamodb.model.ReturnValuesOnConditionCheckFailure;
2831

2932
/**
@@ -215,6 +218,21 @@ public Builder returnValuesOnConditionCheckFailure(String returnValuesOnConditio
215218
return this;
216219
}
217220

221+
/**
222+
* Adds optimistic locking to this transactional delete request.
223+
* <p>
224+
* This method applies a condition expression that ensures the delete operation only succeeds if the version attribute of
225+
* the item matches the provided expected value. If the condition fails, the entire transaction will be cancelled.
226+
*
227+
* @param versionValue the expected version value that must match for the deletion to succeed
228+
* @param versionAttributeName the name of the version attribute in the DynamoDB table
229+
* @return a builder of this type with optimistic locking condition applied
230+
* @throws IllegalArgumentException if any parameter is null
231+
*/
232+
public Builder withOptimisticLocking(AttributeValue versionValue, String versionAttributeName) {
233+
Expression optimisticLockingCondition = createVersionCondition(versionValue, versionAttributeName);
234+
return conditionExpression(optimisticLockingCondition);
235+
}
218236

219237
public TransactDeleteItemEnhancedRequest build() {
220238
return new TransactDeleteItemEnhancedRequest(this);

0 commit comments

Comments
 (0)