Skip to content

Commit a7a2356

Browse files
committed
Optimistic Locking for Delete Operations
1 parent 2f8600c commit a7a2356

File tree

16 files changed

+1669
-8
lines changed

16 files changed

+1669
-8
lines changed

services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/DynamoDbEnhancedIntegrationTestBase.java

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

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

18+
import static software.amazon.awssdk.enhanced.dynamodb.extensions.VersionedRecordExtension.AttributeTags.versionAttribute;
1819
import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey;
1920
import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primarySortKey;
2021
import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.secondaryPartitionKey;
@@ -27,6 +28,7 @@
2728
import java.util.stream.IntStream;
2829
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema;
2930
import software.amazon.awssdk.enhanced.dynamodb.model.Record;
31+
import software.amazon.awssdk.enhanced.dynamodb.model.VersionedRecord;
3032
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
3133
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
3234
import software.amazon.awssdk.testutils.service.AwsIntegrationTestBase;
@@ -75,6 +77,37 @@ protected static DynamoDbAsyncClient createAsyncDynamoDbClient() {
7577
.setter(Record::setStringAttribute))
7678
.build();
7779

80+
protected static final TableSchema<VersionedRecord> VERSIONED_RECORD_TABLE_SCHEMA =
81+
StaticTableSchema.builder(VersionedRecord.class)
82+
.newItemSupplier(VersionedRecord::new)
83+
.addAttribute(String.class, a -> a.name("id")
84+
.getter(VersionedRecord::getId)
85+
.setter(VersionedRecord::setId)
86+
.tags(primaryPartitionKey(), secondaryPartitionKey("index1")))
87+
.addAttribute(Integer.class, a -> a.name("sort")
88+
.getter(VersionedRecord::getSort)
89+
.setter(VersionedRecord::setSort)
90+
.tags(primarySortKey(), secondarySortKey("index1")))
91+
.addAttribute(Integer.class, a -> a.name("value")
92+
.getter(VersionedRecord::getValue)
93+
.setter(VersionedRecord::setValue))
94+
.addAttribute(String.class, a -> a.name("gsi_id")
95+
.getter(VersionedRecord::getGsiId)
96+
.setter(VersionedRecord::setGsiId)
97+
.tags(secondaryPartitionKey("gsi_keys_only")))
98+
.addAttribute(Integer.class, a -> a.name("gsi_sort")
99+
.getter(VersionedRecord::getGsiSort)
100+
.setter(VersionedRecord::setGsiSort)
101+
.tags(secondarySortKey("gsi_keys_only")))
102+
.addAttribute(String.class, a -> a.name("stringAttribute")
103+
.getter(VersionedRecord::getStringAttribute)
104+
.setter(VersionedRecord::setStringAttribute))
105+
.addAttribute(Integer.class, a -> a.name("version")
106+
.getter(VersionedRecord::getVersion)
107+
.setter(VersionedRecord::setVersion)
108+
.tags(versionAttribute()))
109+
.build();
110+
78111

79112
protected static final List<Record> RECORDS =
80113
IntStream.range(0, 9)

services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/OptimisticLockingAsyncCrudIntegrationTest.java

Lines changed: 345 additions & 0 deletions
Large diffs are not rendered by default.

services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/OptimisticLockingCrudIntegrationTest.java

Lines changed: 341 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
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.Objects;
19+
import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbVersionAttribute;
20+
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;
21+
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey;
22+
23+
@DynamoDbBean
24+
public class VersionedRecord {
25+
26+
private String id;
27+
private Integer sort;
28+
private Integer value;
29+
private String gsiId;
30+
private Integer gsiSort;
31+
32+
private String stringAttribute;
33+
private Integer version;
34+
35+
@DynamoDbPartitionKey
36+
public String getId() {
37+
return id;
38+
}
39+
40+
public VersionedRecord setId(String id) {
41+
this.id = id;
42+
return this;
43+
}
44+
45+
public Integer getSort() {
46+
return sort;
47+
}
48+
49+
public VersionedRecord setSort(Integer sort) {
50+
this.sort = sort;
51+
return this;
52+
}
53+
54+
public Integer getValue() {
55+
return value;
56+
}
57+
58+
public VersionedRecord setValue(Integer value) {
59+
this.value = value;
60+
return this;
61+
}
62+
63+
public String getGsiId() {
64+
return gsiId;
65+
}
66+
67+
public VersionedRecord setGsiId(String gsiId) {
68+
this.gsiId = gsiId;
69+
return this;
70+
}
71+
72+
public Integer getGsiSort() {
73+
return gsiSort;
74+
}
75+
76+
public VersionedRecord setGsiSort(Integer gsiSort) {
77+
this.gsiSort = gsiSort;
78+
return this;
79+
}
80+
81+
public String getStringAttribute() {
82+
return stringAttribute;
83+
}
84+
85+
public VersionedRecord setStringAttribute(String stringAttribute) {
86+
this.stringAttribute = stringAttribute;
87+
return this;
88+
}
89+
90+
@DynamoDbVersionAttribute
91+
public Integer getVersion() {
92+
return version;
93+
}
94+
95+
public VersionedRecord setVersion(Integer version) {
96+
this.version = version;
97+
return this;
98+
}
99+
100+
@Override
101+
public boolean equals(Object o) {
102+
if (this == o) {
103+
return true;
104+
}
105+
if (o == null || getClass() != o.getClass()) {
106+
return false;
107+
}
108+
VersionedRecord versionedRecord = (VersionedRecord) o;
109+
return Objects.equals(id, versionedRecord.id) &&
110+
Objects.equals(sort, versionedRecord.sort) &&
111+
Objects.equals(value, versionedRecord.value) &&
112+
Objects.equals(gsiId, versionedRecord.gsiId) &&
113+
Objects.equals(stringAttribute, versionedRecord.stringAttribute) &&
114+
Objects.equals(gsiSort, versionedRecord.gsiSort) &&
115+
Objects.equals(version, versionedRecord.version);
116+
}
117+
118+
@Override
119+
public int hashCode() {
120+
return Objects.hash(id, sort, value, gsiId, gsiSort, stringAttribute, version);
121+
}
122+
}

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: 29 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,54 @@ 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+
public CompletableFuture<T> deleteItem(T keyItem, boolean useOptimisticLocking) {
173+
DeleteItemEnhancedRequest request = DeleteItemEnhancedRequest.builder().key(keyFrom(keyItem)).build();
174+
request = conditionallyApplyOptimisticLocking(request, keyItem, tableSchema, useOptimisticLocking);
175+
return deleteItem(request);
149176
}
150177

151178
@Override
@@ -311,7 +338,7 @@ public CompletableFuture<T> updateItem(T item) {
311338
public Key keyFrom(T item) {
312339
return createKeyFromItem(item, tableSchema, TableMetadata.primaryIndexName());
313340
}
314-
341+
315342

316343
@Override
317344
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
}

0 commit comments

Comments
 (0)