Skip to content

Commit 90dab7c

Browse files
committed
Optimistic locking for delete scenario with TransactWriteItemsEnhancedRequest
1 parent e58ae12 commit 90dab7c

File tree

15 files changed

+586
-3
lines changed

15 files changed

+586
-3
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": "Optimistic locking while using DynamoDbEnhancedClient - DeleteItem with TransactWriteItemsEnhancedRequest"
6+
}

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

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

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

18-
import static org.assertj.core.api.Assertions.as;
1918
import static org.assertj.core.api.Assertions.assertThat;
2019
import static org.assertj.core.api.Assertions.assertThatThrownBy;
2120

@@ -31,6 +30,8 @@
3130
import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedRequest;
3231
import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedResponse;
3332
import software.amazon.awssdk.enhanced.dynamodb.model.Record;
33+
import software.amazon.awssdk.enhanced.dynamodb.model.RecordWithVersion;
34+
import software.amazon.awssdk.enhanced.dynamodb.model.TransactWriteItemsEnhancedRequest;
3435
import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedRequest;
3536
import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedResponse;
3637
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
@@ -41,6 +42,7 @@
4142
import software.amazon.awssdk.services.dynamodb.model.ReturnItemCollectionMetrics;
4243
import software.amazon.awssdk.services.dynamodb.model.ReturnValue;
4344
import software.amazon.awssdk.services.dynamodb.model.ReturnValuesOnConditionCheckFailure;
45+
import software.amazon.awssdk.services.dynamodb.model.TransactionCanceledException;
4446

4547
public class AsyncCrudWithResponseIntegrationTest extends DynamoDbEnhancedIntegrationTestBase {
4648

@@ -56,13 +58,15 @@ public class AsyncCrudWithResponseIntegrationTest extends DynamoDbEnhancedIntegr
5658
private static DynamoDbAsyncClient dynamoDbClient;
5759
private static DynamoDbEnhancedAsyncClient enhancedClient;
5860
private static DynamoDbAsyncTable<Record> mappedTable;
61+
private static DynamoDbAsyncTable<RecordWithVersion> recordWithVersionMappedTable;
5962

6063
@BeforeClass
6164
public static void beforeClass() {
6265
dynamoDbClient = createAsyncDynamoDbClient();
6366
enhancedClient = DynamoDbEnhancedAsyncClient.builder().dynamoDbClient(dynamoDbClient).build();
6467
mappedTable = enhancedClient.table(TABLE_NAME, TABLE_SCHEMA);
6568
mappedTable.createTable(r -> r.localSecondaryIndices(LOCAL_SECONDARY_INDEX)).join();
69+
recordWithVersionMappedTable = enhancedClient.table(TABLE_NAME, RECORD_WITH_VERSION_TABLE_SCHEMA);
6670
dynamoDbClient.waiter().waitUntilTableExists(r -> r.tableName(TABLE_NAME)).join();
6771
}
6872

@@ -341,4 +345,61 @@ public void getItem_withoutReturnConsumedCapacity() {
341345
GetItemEnhancedResponse<Record> response = mappedTable.getItemWithResponse(req -> req.key(key)).join();
342346
assertThat(response.consumedCapacity()).isNull();
343347
}
348+
349+
@Test
350+
public void deleteItemWithOptimisticLockingEnabled_shouldSucceedIfVersionMatch() {
351+
RecordWithVersion originalItem = new RecordWithVersion().setId("123").setSort(10).setStringAttribute("Original Item");
352+
Key recordKey = Key.builder()
353+
.partitionValue(originalItem.getId())
354+
.sortValue(originalItem.getSort())
355+
.build();
356+
recordWithVersionMappedTable.putItem(originalItem).join();
357+
358+
// Retrieve the item
359+
RecordWithVersion retrievedItem = recordWithVersionMappedTable.getItem(r -> r.key(recordKey)).join();
360+
361+
// Delete the item using a transaction
362+
TransactWriteItemsEnhancedRequest request = TransactWriteItemsEnhancedRequest.builder()
363+
.addDeleteItem(recordWithVersionMappedTable, retrievedItem)
364+
.build();
365+
366+
enhancedClient.transactWriteItems(request).join();
367+
368+
RecordWithVersion deletedItem = recordWithVersionMappedTable.getItem(r -> r.key(recordKey)).join();
369+
assertThat(deletedItem).isNull();
370+
}
371+
372+
@Test
373+
public void deleteItemWithOptimisticLockingEnabled_shouldFailIfVersionMismatch() {
374+
RecordWithVersion originalItem = new RecordWithVersion().setId("123").setSort(10).setStringAttribute("Original Item");
375+
Key recordKey = Key.builder()
376+
.partitionValue(originalItem.getId())
377+
.sortValue(originalItem.getSort())
378+
.build();
379+
380+
recordWithVersionMappedTable.putItem(originalItem).join();
381+
382+
// Retrieve the item and modify it separately
383+
RecordWithVersion modifiedItem = recordWithVersionMappedTable.getItem(r -> r.key(recordKey)).join();
384+
modifiedItem.setStringAttribute("Updated Item");
385+
386+
// Update the item, which will increment the version
387+
recordWithVersionMappedTable.updateItem(modifiedItem).join();
388+
389+
// Now attempt to delete the original item using a transaction
390+
TransactWriteItemsEnhancedRequest request = TransactWriteItemsEnhancedRequest.builder()
391+
.addDeleteItem(recordWithVersionMappedTable, modifiedItem)
392+
.build();
393+
394+
assertThatThrownBy(() -> enhancedClient.transactWriteItems(request).join())
395+
.isInstanceOf(CompletionException.class)
396+
.satisfies(e ->
397+
assertThat(((TransactionCanceledException) e.getCause())
398+
.cancellationReasons()
399+
.stream()
400+
.anyMatch(reason ->
401+
"ConditionalCheckFailed".equals(reason.code())
402+
&& "The conditional request failed".equals(reason.message())))
403+
.isTrue());
404+
}
344405
}

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

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717

1818
import static org.assertj.core.api.Assertions.assertThat;
1919
import static org.assertj.core.api.Assertions.assertThatThrownBy;
20+
import static org.junit.Assert.assertEquals;
21+
import static org.junit.Assert.assertThrows;
22+
import static org.junit.Assert.assertTrue;
2023

2124
import org.assertj.core.data.Offset;
2225
import org.junit.After;
@@ -30,6 +33,8 @@
3033
import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedRequest;
3134
import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedResponse;
3235
import software.amazon.awssdk.enhanced.dynamodb.model.Record;
36+
import software.amazon.awssdk.enhanced.dynamodb.model.RecordWithVersion;
37+
import software.amazon.awssdk.enhanced.dynamodb.model.TransactWriteItemsEnhancedRequest;
3338
import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedRequest;
3439
import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedResponse;
3540
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
@@ -40,6 +45,7 @@
4045
import software.amazon.awssdk.services.dynamodb.model.ReturnConsumedCapacity;
4146
import software.amazon.awssdk.services.dynamodb.model.ReturnItemCollectionMetrics;
4247
import software.amazon.awssdk.services.dynamodb.model.ReturnValuesOnConditionCheckFailure;
48+
import software.amazon.awssdk.services.dynamodb.model.TransactionCanceledException;
4349

4450
public class CrudWithResponseIntegrationTest extends DynamoDbEnhancedIntegrationTestBase {
4551

@@ -56,13 +62,15 @@ public class CrudWithResponseIntegrationTest extends DynamoDbEnhancedIntegration
5662
private static DynamoDbClient dynamoDbClient;
5763
private static DynamoDbEnhancedClient enhancedClient;
5864
private static DynamoDbTable<Record> mappedTable;
65+
private static DynamoDbTable<RecordWithVersion> recordWithVersionMappedTable;
5966

6067
@BeforeClass
6168
public static void beforeClass() {
6269
dynamoDbClient = createDynamoDbClient();
6370
enhancedClient = DynamoDbEnhancedClient.builder().dynamoDbClient(dynamoDbClient).build();
6471
mappedTable = enhancedClient.table(TABLE_NAME, TABLE_SCHEMA);
6572
mappedTable.createTable(r -> r.localSecondaryIndices(LOCAL_SECONDARY_INDEX));
73+
recordWithVersionMappedTable = enhancedClient.table(TABLE_NAME, RECORD_WITH_VERSION_TABLE_SCHEMA);
6674
dynamoDbClient.waiter().waitUntilTableExists(r -> r.tableName(TABLE_NAME));
6775
}
6876

@@ -309,4 +317,59 @@ public void getItem_set_stronglyConsistent() {
309317
// A strongly consistent read request of an item up to 4 KB requires one read request unit.
310318
assertThat(consumedCapacity.capacityUnits()).isCloseTo(20.0, Offset.offset(1.0));
311319
}
320+
321+
@Test
322+
public void deleteItemWithOptimisticLockingEnabled_shouldSucceedIfVersionMatch() {
323+
RecordWithVersion originalItem = new RecordWithVersion().setId("123").setSort(10).setStringAttribute("Original Item");
324+
Key recordKey = Key.builder()
325+
.partitionValue(originalItem.getId())
326+
.sortValue(originalItem.getSort())
327+
.build();
328+
recordWithVersionMappedTable.putItem(originalItem);
329+
330+
// Retrieve the item
331+
RecordWithVersion retrievedItem = recordWithVersionMappedTable.getItem(r -> r.key(recordKey));
332+
333+
// Delete the item using a transaction
334+
TransactWriteItemsEnhancedRequest request = TransactWriteItemsEnhancedRequest.builder()
335+
.addDeleteItem(recordWithVersionMappedTable, retrievedItem)
336+
.build();
337+
338+
enhancedClient.transactWriteItems(request);
339+
340+
RecordWithVersion deletedItem = recordWithVersionMappedTable.getItem(r -> r.key(recordKey));
341+
assertThat(deletedItem).isNull();
342+
}
343+
344+
@Test
345+
public void deleteItemWithOptimisticLockingEnabled_shouldFailIfVersionMismatch() {
346+
RecordWithVersion originalItem = new RecordWithVersion().setId("123").setSort(10).setStringAttribute("Original Item");
347+
Key recordKey = Key.builder()
348+
.partitionValue(originalItem.getId())
349+
.sortValue(originalItem.getSort())
350+
.build();
351+
352+
recordWithVersionMappedTable.putItem(originalItem);
353+
354+
// Retrieve the item and modify it separately
355+
RecordWithVersion modifiedItem = recordWithVersionMappedTable.getItem(r -> r.key(recordKey));
356+
modifiedItem.setStringAttribute("Updated Item");
357+
358+
// Update the item, which will increment the version
359+
recordWithVersionMappedTable.updateItem(modifiedItem);
360+
361+
// Now attempt to delete the original item using a transaction
362+
TransactWriteItemsEnhancedRequest request = TransactWriteItemsEnhancedRequest.builder()
363+
.addDeleteItem(recordWithVersionMappedTable, modifiedItem)
364+
.build();
365+
366+
TransactionCanceledException ex = assertThrows(
367+
TransactionCanceledException.class,
368+
() -> enhancedClient.transactWriteItems(request));
369+
370+
assertTrue(ex.hasCancellationReasons());
371+
assertEquals(1, ex.cancellationReasons().size());
372+
assertEquals("ConditionalCheckFailed", ex.cancellationReasons().get(0).code());
373+
assertEquals("The conditional request failed", ex.cancellationReasons().get(0).message());
374+
}
312375
}

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

79112
protected static final List<Record> RECORDS =
80113
IntStream.range(0, 9)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
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+
22+
@DynamoDbBean
23+
public class RecordWithVersion {
24+
25+
private String id;
26+
private Integer sort;
27+
private Integer value;
28+
private String gsiId;
29+
private Integer gsiSort;
30+
31+
private String stringAttribute;
32+
private Integer version;
33+
34+
public String getId() {
35+
return id;
36+
}
37+
38+
public RecordWithVersion setId(String id) {
39+
this.id = id;
40+
return this;
41+
}
42+
43+
public Integer getSort() {
44+
return sort;
45+
}
46+
47+
public RecordWithVersion setSort(Integer sort) {
48+
this.sort = sort;
49+
return this;
50+
}
51+
52+
public Integer getValue() {
53+
return value;
54+
}
55+
56+
public RecordWithVersion setValue(Integer value) {
57+
this.value = value;
58+
return this;
59+
}
60+
61+
public String getGsiId() {
62+
return gsiId;
63+
}
64+
65+
public RecordWithVersion setGsiId(String gsiId) {
66+
this.gsiId = gsiId;
67+
return this;
68+
}
69+
70+
public Integer getGsiSort() {
71+
return gsiSort;
72+
}
73+
74+
public RecordWithVersion setGsiSort(Integer gsiSort) {
75+
this.gsiSort = gsiSort;
76+
return this;
77+
}
78+
79+
public String getStringAttribute() {
80+
return stringAttribute;
81+
}
82+
83+
public RecordWithVersion setStringAttribute(String stringAttribute) {
84+
this.stringAttribute = stringAttribute;
85+
return this;
86+
}
87+
88+
@DynamoDbVersionAttribute
89+
public Integer getVersion() {
90+
return version;
91+
}
92+
93+
public RecordWithVersion setVersion(Integer version) {
94+
this.version = version;
95+
return this;
96+
}
97+
98+
@Override
99+
public boolean equals(Object o) {
100+
if (this == o) {
101+
return true;
102+
}
103+
if (o == null || getClass() != o.getClass()) {
104+
return false;
105+
}
106+
RecordWithVersion recordWithVersion = (RecordWithVersion) o;
107+
return Objects.equals(id, recordWithVersion.id) &&
108+
Objects.equals(sort, recordWithVersion.sort) &&
109+
Objects.equals(value, recordWithVersion.value) &&
110+
Objects.equals(gsiId, recordWithVersion.gsiId) &&
111+
Objects.equals(stringAttribute, recordWithVersion.stringAttribute) &&
112+
Objects.equals(gsiSort, recordWithVersion.gsiSort) &&
113+
Objects.equals(version, recordWithVersion.version);
114+
}
115+
116+
@Override
117+
public int hashCode() {
118+
return Objects.hash(id, sort, value, gsiId, gsiSort, stringAttribute, version);
119+
}
120+
}

0 commit comments

Comments
 (0)