Skip to content

Commit f6e85cb

Browse files
committed
Optimistic locking for delete scenario with TransactWriteItemsEnhancedRequest
1 parent 87d60cd commit f6e85cb

File tree

7 files changed

+294
-75
lines changed

7 files changed

+294
-75
lines changed

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

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedRequest;
3131
import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedResponse;
3232
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;
3335
import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedRequest;
3436
import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedResponse;
3537
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
@@ -40,6 +42,7 @@
4042
import software.amazon.awssdk.services.dynamodb.model.ReturnItemCollectionMetrics;
4143
import software.amazon.awssdk.services.dynamodb.model.ReturnValue;
4244
import software.amazon.awssdk.services.dynamodb.model.ReturnValuesOnConditionCheckFailure;
45+
import software.amazon.awssdk.services.dynamodb.model.TransactionCanceledException;
4346

4447
public class AsyncCrudWithResponseIntegrationTest extends DynamoDbEnhancedIntegrationTestBase {
4548

@@ -55,13 +58,15 @@ public class AsyncCrudWithResponseIntegrationTest extends DynamoDbEnhancedIntegr
5558
private static DynamoDbAsyncClient dynamoDbClient;
5659
private static DynamoDbEnhancedAsyncClient enhancedClient;
5760
private static DynamoDbAsyncTable<Record> mappedTable;
61+
private static DynamoDbAsyncTable<RecordWithVersion> recordWithVersionMappedTable;
5862

5963
@BeforeClass
6064
public static void beforeClass() {
6165
dynamoDbClient = createAsyncDynamoDbClient();
6266
enhancedClient = DynamoDbEnhancedAsyncClient.builder().dynamoDbClient(dynamoDbClient).build();
6367
mappedTable = enhancedClient.table(TABLE_NAME, TABLE_SCHEMA);
6468
mappedTable.createTable(r -> r.localSecondaryIndices(LOCAL_SECONDARY_INDEX)).join();
69+
recordWithVersionMappedTable = enhancedClient.table(TABLE_NAME, RECORD_WITH_VERSION_TABLE_SCHEMA);
6570
dynamoDbClient.waiter().waitUntilTableExists(r -> r.tableName(TABLE_NAME)).join();
6671
}
6772

@@ -191,7 +196,7 @@ public void updateItem_returnValues_all_old() {
191196
Record record = new Record().setId("1").setSort(10);
192197
mappedTable.putItem(record).join();
193198

194-
Record updatedRecord = new Record().setId("1").setSort(10).setValue(11).setVersion(1);
199+
Record updatedRecord = new Record().setId("1").setSort(10).setValue(11);
195200

196201

197202
UpdateItemEnhancedResponse<Record> response = mappedTable.updateItemWithResponse(r -> r.item(updatedRecord)
@@ -201,15 +206,14 @@ public void updateItem_returnValues_all_old() {
201206
assertThat(response.attributes().getId()).isEqualTo(record.getId());
202207
assertThat(response.attributes().getSort()).isEqualTo(record.getSort());
203208
assertThat(response.attributes().getValue()).isEqualTo(null);
204-
assertThat(response.attributes().getVersion()).isEqualTo(1);
205209
}
206210

207211
@Test
208212
public void updateItem_returnValues_all_new() {
209213
Record record = new Record().setId("1").setSort(10);
210214
mappedTable.putItem(record).join();
211215

212-
Record updatedRecord = new Record().setId("1").setSort(10).setValue(11).setVersion(1);
216+
Record updatedRecord = new Record().setId("1").setSort(10).setValue(11);
213217

214218

215219
UpdateItemEnhancedResponse<Record> response = mappedTable.updateItemWithResponse(r -> r.item(updatedRecord)
@@ -219,15 +223,14 @@ public void updateItem_returnValues_all_new() {
219223
assertThat(response.attributes().getId()).isEqualTo(updatedRecord.getId());
220224
assertThat(response.attributes().getSort()).isEqualTo(updatedRecord.getSort());
221225
assertThat(response.attributes().getValue()).isEqualTo(updatedRecord.getValue());
222-
assertThat(response.attributes().getVersion()).isEqualTo(updatedRecord.getVersion() + 1);
223226
}
224227

225228
@Test
226229
public void updateItem_returnValues_not_set() {
227230
Record record = new Record().setId("1").setSort(10);
228231
mappedTable.putItem(record).join();
229232

230-
Record updatedRecord = new Record().setId("1").setSort(10).setValue(11).setVersion(1);
233+
Record updatedRecord = new Record().setId("1").setSort(10).setValue(11);
231234

232235

233236
UpdateItemEnhancedResponse<Record> response = mappedTable.updateItemWithResponse(r -> r.item(updatedRecord))
@@ -236,7 +239,6 @@ public void updateItem_returnValues_not_set() {
236239
assertThat(response.attributes().getId()).isEqualTo(updatedRecord.getId());
237240
assertThat(response.attributes().getSort()).isEqualTo(updatedRecord.getSort());
238241
assertThat(response.attributes().getValue()).isEqualTo(updatedRecord.getValue());
239-
assertThat(response.attributes().getVersion()).isEqualTo(updatedRecord.getVersion() + 1);
240242
}
241243

242244
@Test
@@ -343,4 +345,61 @@ public void getItem_withoutReturnConsumedCapacity() {
343345
GetItemEnhancedResponse<Record> response = mappedTable.getItemWithResponse(req -> req.key(key)).join();
344346
assertThat(response.consumedCapacity()).isNull();
345347
}
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+
}
346405
}

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

Lines changed: 60 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +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;
2021
import static org.junit.Assert.assertThrows;
22+
import static org.junit.Assert.assertTrue;
2123

2224
import org.assertj.core.data.Offset;
2325
import org.junit.After;
@@ -31,6 +33,7 @@
3133
import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedRequest;
3234
import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedResponse;
3335
import software.amazon.awssdk.enhanced.dynamodb.model.Record;
36+
import software.amazon.awssdk.enhanced.dynamodb.model.RecordWithVersion;
3437
import software.amazon.awssdk.enhanced.dynamodb.model.TransactWriteItemsEnhancedRequest;
3538
import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedRequest;
3639
import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedResponse;
@@ -59,13 +62,15 @@ public class CrudWithResponseIntegrationTest extends DynamoDbEnhancedIntegration
5962
private static DynamoDbClient dynamoDbClient;
6063
private static DynamoDbEnhancedClient enhancedClient;
6164
private static DynamoDbTable<Record> mappedTable;
65+
private static DynamoDbTable<RecordWithVersion> recordWithVersionMappedTable;
6266

6367
@BeforeClass
6468
public static void beforeClass() {
6569
dynamoDbClient = createDynamoDbClient();
6670
enhancedClient = DynamoDbEnhancedClient.builder().dynamoDbClient(dynamoDbClient).build();
6771
mappedTable = enhancedClient.table(TABLE_NAME, TABLE_SCHEMA);
6872
mappedTable.createTable(r -> r.localSecondaryIndices(LOCAL_SECONDARY_INDEX));
73+
recordWithVersionMappedTable = enhancedClient.table(TABLE_NAME, RECORD_WITH_VERSION_TABLE_SCHEMA);
6974
dynamoDbClient.waiter().waitUntilTableExists(r -> r.tableName(TABLE_NAME));
7075
}
7176

@@ -221,54 +226,6 @@ public void deleteItem_returnValuesOnConditionCheckFailure_set_returnValuesOnCon
221226
.satisfies(e -> assertThat(((ConditionalCheckFailedException) e).hasItem()).isFalse());
222227
}
223228

224-
@Test
225-
public void deleteItemWithTransactWrite_shouldFailIfVersionMismatch() {
226-
Record originalItem = new Record().setId("123").setSort(10).setStringAttribute("Original Item");
227-
Key recordKey = Key.builder()
228-
.partitionValue(originalItem.getId())
229-
.sortValue(originalItem.getSort())
230-
.build();
231-
232-
mappedTable.putItem(originalItem);
233-
234-
// Retrieve the item and modify it separately
235-
Record modifiedItem = mappedTable.getItem(r -> r.key(recordKey));
236-
modifiedItem.setStringAttribute("Updated Item");
237-
238-
// Update the item, which will increment the version
239-
mappedTable.updateItem(modifiedItem);
240-
241-
// Now attempt to delete the original item using a transaction
242-
TransactWriteItemsEnhancedRequest request = TransactWriteItemsEnhancedRequest.builder()
243-
.addDeleteItem(mappedTable, modifiedItem)
244-
.build();
245-
246-
assertThrows(TransactionCanceledException.class, () -> enhancedClient.transactWriteItems(request));
247-
}
248-
249-
@Test
250-
public void deleteItemWithTransactWrite_shouldSucceedIfVersionMatch() {
251-
Record originalItem = new Record().setId("123").setSort(10).setStringAttribute("Original Item");
252-
Key recordKey = Key.builder()
253-
.partitionValue(originalItem.getId())
254-
.sortValue(originalItem.getSort())
255-
.build();
256-
mappedTable.putItem(originalItem);
257-
258-
// Retrieve the item
259-
Record retrievedItem = mappedTable.getItem(r -> r.key(recordKey));
260-
261-
// Delete the item using a transaction
262-
TransactWriteItemsEnhancedRequest request = TransactWriteItemsEnhancedRequest.builder()
263-
.addDeleteItem(mappedTable, retrievedItem)
264-
.build();
265-
266-
enhancedClient.transactWriteItems(request);
267-
268-
Record deletedItem = mappedTable.getItem(r -> r.key(recordKey));
269-
assertThat(deletedItem).isNull();
270-
}
271-
272229
@Test
273230
public void deleteItem_returnValuesOnConditionCheckFailure_set_returnValuesOnConditionCheckFailureNotNull() {
274231
Record record = new Record().setId("1").setSort(10);
@@ -360,4 +317,59 @@ public void getItem_set_stronglyConsistent() {
360317
// A strongly consistent read request of an item up to 4 KB requires one read request unit.
361318
assertThat(consumedCapacity.capacityUnits()).isCloseTo(20.0, Offset.offset(1.0));
362319
}
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+
}
363375
}

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

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import java.util.stream.IntStream;
2929
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema;
3030
import software.amazon.awssdk.enhanced.dynamodb.model.Record;
31+
import software.amazon.awssdk.enhanced.dynamodb.model.RecordWithVersion;
3132
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
3233
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
3334
import software.amazon.awssdk.testutils.service.AwsIntegrationTestBase;
@@ -74,9 +75,36 @@ protected static DynamoDbAsyncClient createAsyncDynamoDbClient() {
7475
.addAttribute(String.class, a -> a.name("stringAttribute")
7576
.getter(Record::getStringAttribute)
7677
.setter(Record::setStringAttribute))
78+
.build();
79+
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))
77105
.addAttribute(Integer.class, a -> a.name("version")
78-
.getter(Record::getVersion)
79-
.setter(Record::setVersion)
106+
.getter(RecordWithVersion::getVersion)
107+
.setter(RecordWithVersion::setVersion)
80108
.tags(versionAttribute()))
81109
.build();
82110

0 commit comments

Comments
 (0)