Skip to content

Commit 32bbcc2

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

File tree

3 files changed

+263
-29
lines changed

3 files changed

+263
-29
lines changed

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

Lines changed: 112 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,55 @@ public void getItem_withoutReturnConsumedCapacity() {
347347
}
348348

349349
@Test
350-
public void deleteItemWithOptimisticLockingEnabled_shouldSucceedIfVersionMatch() {
350+
public void deleteItemWithoutVersion_andOptimisticLockingEnabled_shouldSucceed() {
351+
Record originalItem = new Record().setId("123").setSort(10).setStringAttribute("Original Item");
352+
Key recordKey = Key.builder()
353+
.partitionValue(originalItem.getId())
354+
.sortValue(originalItem.getSort())
355+
.build();
356+
mappedTable.putItem(originalItem).join();
357+
358+
// Retrieve the item
359+
Record retrievedItem = mappedTable.getItem(r -> r.key(recordKey)).join();
360+
361+
// Delete the item using a transaction
362+
TransactWriteItemsEnhancedRequest request =
363+
TransactWriteItemsEnhancedRequest.builder()
364+
.addDeleteItem(mappedTable, retrievedItem, true)
365+
.build();
366+
367+
enhancedClient.transactWriteItems(request).join();
368+
369+
Record deletedItem = mappedTable.getItem(r -> r.key(recordKey)).join();
370+
assertThat(deletedItem).isNull();
371+
}
372+
373+
@Test
374+
public void deleteItemWithoutVersion_andOptimisticLockingDisabled_shouldSucceed() {
375+
Record originalItem = new Record().setId("123").setSort(10).setStringAttribute("Original Item");
376+
Key recordKey = Key.builder()
377+
.partitionValue(originalItem.getId())
378+
.sortValue(originalItem.getSort())
379+
.build();
380+
mappedTable.putItem(originalItem).join();
381+
382+
// Retrieve the item
383+
Record retrievedItem = mappedTable.getItem(r -> r.key(recordKey)).join();
384+
385+
// Delete the item using a transaction
386+
TransactWriteItemsEnhancedRequest request =
387+
TransactWriteItemsEnhancedRequest.builder()
388+
.addDeleteItem(mappedTable, retrievedItem, false)
389+
.build();
390+
391+
enhancedClient.transactWriteItems(request).join();
392+
393+
Record deletedItem = mappedTable.getItem(r -> r.key(recordKey)).join();
394+
assertThat(deletedItem).isNull();
395+
}
396+
397+
@Test
398+
public void deleteItemWithVersion_andOptimisticLockingEnabled_ifVersionMatch_shouldSucceed() {
351399
RecordWithVersion originalItem = new RecordWithVersion().setId("123").setSort(10).setStringAttribute("Original Item");
352400
Key recordKey = Key.builder()
353401
.partitionValue(originalItem.getId())
@@ -359,9 +407,10 @@ public void deleteItemWithOptimisticLockingEnabled_shouldSucceedIfVersionMatch()
359407
RecordWithVersion retrievedItem = recordWithVersionMappedTable.getItem(r -> r.key(recordKey)).join();
360408

361409
// Delete the item using a transaction
362-
TransactWriteItemsEnhancedRequest request = TransactWriteItemsEnhancedRequest.builder()
363-
.addDeleteItem(recordWithVersionMappedTable, retrievedItem)
364-
.build();
410+
TransactWriteItemsEnhancedRequest request =
411+
TransactWriteItemsEnhancedRequest.builder()
412+
.addDeleteItem(recordWithVersionMappedTable, retrievedItem, true)
413+
.build();
365414

366415
enhancedClient.transactWriteItems(request).join();
367416

@@ -370,7 +419,7 @@ public void deleteItemWithOptimisticLockingEnabled_shouldSucceedIfVersionMatch()
370419
}
371420

372421
@Test
373-
public void deleteItemWithOptimisticLockingEnabled_shouldFailIfVersionMismatch() {
422+
public void deleteItemWithVersion_andOptimisticLockingEnabled_ifVersionMismatch_shouldFail() {
374423
RecordWithVersion originalItem = new RecordWithVersion().setId("123").setSort(10).setStringAttribute("Original Item");
375424
Key recordKey = Key.builder()
376425
.partitionValue(originalItem.getId())
@@ -384,12 +433,13 @@ public void deleteItemWithOptimisticLockingEnabled_shouldFailIfVersionMismatch()
384433
modifiedItem.setStringAttribute("Updated Item");
385434

386435
// Update the item, which will increment the version
387-
recordWithVersionMappedTable.updateItem(modifiedItem).join();
436+
recordWithVersionMappedTable.updateItem(modifiedItem);
388437

389438
// Now attempt to delete the original item using a transaction
390-
TransactWriteItemsEnhancedRequest request = TransactWriteItemsEnhancedRequest.builder()
391-
.addDeleteItem(recordWithVersionMappedTable, modifiedItem)
392-
.build();
439+
TransactWriteItemsEnhancedRequest request =
440+
TransactWriteItemsEnhancedRequest.builder()
441+
.addDeleteItem(recordWithVersionMappedTable, modifiedItem, true)
442+
.build();
393443

394444
assertThatThrownBy(() -> enhancedClient.transactWriteItems(request).join())
395445
.isInstanceOf(CompletionException.class)
@@ -402,4 +452,57 @@ public void deleteItemWithOptimisticLockingEnabled_shouldFailIfVersionMismatch()
402452
&& "The conditional request failed".equals(reason.message())))
403453
.isTrue());
404454
}
455+
456+
@Test
457+
public void deleteItemWithVersion_andOptimisticLockingDisabled_ifVersionMatch_shouldSucceed() {
458+
RecordWithVersion originalItem = new RecordWithVersion().setId("123").setSort(10).setStringAttribute("Original Item");
459+
Key recordKey = Key.builder()
460+
.partitionValue(originalItem.getId())
461+
.sortValue(originalItem.getSort())
462+
.build();
463+
recordWithVersionMappedTable.putItem(originalItem).join();
464+
465+
// Retrieve the item
466+
RecordWithVersion retrievedItem = recordWithVersionMappedTable.getItem(r -> r.key(recordKey)).join();
467+
468+
// Delete the item using a transaction
469+
TransactWriteItemsEnhancedRequest request =
470+
TransactWriteItemsEnhancedRequest.builder()
471+
.addDeleteItem(recordWithVersionMappedTable, retrievedItem, false)
472+
.build();
473+
474+
enhancedClient.transactWriteItems(request).join();
475+
476+
RecordWithVersion deletedItem = recordWithVersionMappedTable.getItem(r -> r.key(recordKey)).join();
477+
assertThat(deletedItem).isNull();
478+
}
479+
480+
@Test
481+
public void deleteItemWithVersion_andOptimisticLockingDisabled_ifVersionMismatch_shouldSucceed() {
482+
RecordWithVersion originalItem = new RecordWithVersion().setId("123").setSort(10).setStringAttribute("Original Item");
483+
Key recordKey = Key.builder()
484+
.partitionValue(originalItem.getId())
485+
.sortValue(originalItem.getSort())
486+
.build();
487+
488+
recordWithVersionMappedTable.putItem(originalItem).join();
489+
490+
// Retrieve the item and modify it separately
491+
RecordWithVersion modifiedItem = recordWithVersionMappedTable.getItem(r -> r.key(recordKey)).join();
492+
modifiedItem.setStringAttribute("Updated Item");
493+
494+
// Update the item, which will increment the version
495+
recordWithVersionMappedTable.updateItem(modifiedItem);
496+
497+
// Now attempt to delete the original item using a transaction
498+
TransactWriteItemsEnhancedRequest request =
499+
TransactWriteItemsEnhancedRequest.builder()
500+
.addDeleteItem(recordWithVersionMappedTable, modifiedItem, false)
501+
.build();
502+
503+
enhancedClient.transactWriteItems(request).join();
504+
505+
RecordWithVersion deletedItem = recordWithVersionMappedTable.getItem(r -> r.key(recordKey)).join();
506+
assertThat(deletedItem).isNull();
507+
}
405508
}

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

Lines changed: 111 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,55 @@ public void getItem_set_stronglyConsistent() {
319319
}
320320

321321
@Test
322-
public void deleteItemWithOptimisticLockingEnabled_shouldSucceedIfVersionMatch() {
322+
public void deleteItemWithoutVersion_andOptimisticLockingEnabled_shouldSucceed() {
323+
Record originalItem = new Record().setId("123").setSort(10).setStringAttribute("Original Item");
324+
Key recordKey = Key.builder()
325+
.partitionValue(originalItem.getId())
326+
.sortValue(originalItem.getSort())
327+
.build();
328+
mappedTable.putItem(originalItem);
329+
330+
// Retrieve the item
331+
Record retrievedItem = mappedTable.getItem(r -> r.key(recordKey));
332+
333+
// Delete the item using a transaction
334+
TransactWriteItemsEnhancedRequest request =
335+
TransactWriteItemsEnhancedRequest.builder()
336+
.addDeleteItem(mappedTable, retrievedItem, true)
337+
.build();
338+
339+
enhancedClient.transactWriteItems(request);
340+
341+
Record deletedItem = mappedTable.getItem(r -> r.key(recordKey));
342+
assertThat(deletedItem).isNull();
343+
}
344+
345+
@Test
346+
public void deleteItemWithoutVersion_andOptimisticLockingDisabled_shouldSucceed() {
347+
Record originalItem = new Record().setId("123").setSort(10).setStringAttribute("Original Item");
348+
Key recordKey = Key.builder()
349+
.partitionValue(originalItem.getId())
350+
.sortValue(originalItem.getSort())
351+
.build();
352+
mappedTable.putItem(originalItem);
353+
354+
// Retrieve the item
355+
Record retrievedItem = mappedTable.getItem(r -> r.key(recordKey));
356+
357+
// Delete the item using a transaction
358+
TransactWriteItemsEnhancedRequest request =
359+
TransactWriteItemsEnhancedRequest.builder()
360+
.addDeleteItem(mappedTable, retrievedItem, false)
361+
.build();
362+
363+
enhancedClient.transactWriteItems(request);
364+
365+
Record deletedItem = mappedTable.getItem(r -> r.key(recordKey));
366+
assertThat(deletedItem).isNull();
367+
}
368+
369+
@Test
370+
public void deleteItemWithVersion_andOptimisticLockingEnabled_ifVersionMatch_shouldSucceed() {
323371
RecordWithVersion originalItem = new RecordWithVersion().setId("123").setSort(10).setStringAttribute("Original Item");
324372
Key recordKey = Key.builder()
325373
.partitionValue(originalItem.getId())
@@ -331,9 +379,10 @@ public void deleteItemWithOptimisticLockingEnabled_shouldSucceedIfVersionMatch()
331379
RecordWithVersion retrievedItem = recordWithVersionMappedTable.getItem(r -> r.key(recordKey));
332380

333381
// Delete the item using a transaction
334-
TransactWriteItemsEnhancedRequest request = TransactWriteItemsEnhancedRequest.builder()
335-
.addDeleteItem(recordWithVersionMappedTable, retrievedItem)
336-
.build();
382+
TransactWriteItemsEnhancedRequest request =
383+
TransactWriteItemsEnhancedRequest.builder()
384+
.addDeleteItem(recordWithVersionMappedTable, retrievedItem, true)
385+
.build();
337386

338387
enhancedClient.transactWriteItems(request);
339388

@@ -342,7 +391,7 @@ public void deleteItemWithOptimisticLockingEnabled_shouldSucceedIfVersionMatch()
342391
}
343392

344393
@Test
345-
public void deleteItemWithOptimisticLockingEnabled_shouldFailIfVersionMismatch() {
394+
public void deleteItemWithVersion_andOptimisticLockingEnabled_ifVersionMismatch_shouldFail() {
346395
RecordWithVersion originalItem = new RecordWithVersion().setId("123").setSort(10).setStringAttribute("Original Item");
347396
Key recordKey = Key.builder()
348397
.partitionValue(originalItem.getId())
@@ -359,9 +408,10 @@ public void deleteItemWithOptimisticLockingEnabled_shouldFailIfVersionMismatch()
359408
recordWithVersionMappedTable.updateItem(modifiedItem);
360409

361410
// Now attempt to delete the original item using a transaction
362-
TransactWriteItemsEnhancedRequest request = TransactWriteItemsEnhancedRequest.builder()
363-
.addDeleteItem(recordWithVersionMappedTable, modifiedItem)
364-
.build();
411+
TransactWriteItemsEnhancedRequest request =
412+
TransactWriteItemsEnhancedRequest.builder()
413+
.addDeleteItem(recordWithVersionMappedTable, modifiedItem, true)
414+
.build();
365415

366416
TransactionCanceledException ex = assertThrows(
367417
TransactionCanceledException.class,
@@ -372,4 +422,57 @@ public void deleteItemWithOptimisticLockingEnabled_shouldFailIfVersionMismatch()
372422
assertEquals("ConditionalCheckFailed", ex.cancellationReasons().get(0).code());
373423
assertEquals("The conditional request failed", ex.cancellationReasons().get(0).message());
374424
}
425+
426+
@Test
427+
public void deleteItemWithVersion_andOptimisticLockingDisabled_ifVersionMatch_shouldSucceed() {
428+
RecordWithVersion originalItem = new RecordWithVersion().setId("123").setSort(10).setStringAttribute("Original Item");
429+
Key recordKey = Key.builder()
430+
.partitionValue(originalItem.getId())
431+
.sortValue(originalItem.getSort())
432+
.build();
433+
recordWithVersionMappedTable.putItem(originalItem);
434+
435+
// Retrieve the item
436+
RecordWithVersion retrievedItem = recordWithVersionMappedTable.getItem(r -> r.key(recordKey));
437+
438+
// Delete the item using a transaction
439+
TransactWriteItemsEnhancedRequest request =
440+
TransactWriteItemsEnhancedRequest.builder()
441+
.addDeleteItem(recordWithVersionMappedTable, retrievedItem, false)
442+
.build();
443+
444+
enhancedClient.transactWriteItems(request);
445+
446+
RecordWithVersion deletedItem = recordWithVersionMappedTable.getItem(r -> r.key(recordKey));
447+
assertThat(deletedItem).isNull();
448+
}
449+
450+
@Test
451+
public void deleteItemWithVersion_andOptimisticLockingDisabled_ifVersionMismatch_shouldSucceed() {
452+
RecordWithVersion originalItem = new RecordWithVersion().setId("123").setSort(10).setStringAttribute("Original Item");
453+
Key recordKey = Key.builder()
454+
.partitionValue(originalItem.getId())
455+
.sortValue(originalItem.getSort())
456+
.build();
457+
458+
recordWithVersionMappedTable.putItem(originalItem);
459+
460+
// Retrieve the item and modify it separately
461+
RecordWithVersion modifiedItem = recordWithVersionMappedTable.getItem(r -> r.key(recordKey));
462+
modifiedItem.setStringAttribute("Updated Item");
463+
464+
// Update the item, which will increment the version
465+
recordWithVersionMappedTable.updateItem(modifiedItem);
466+
467+
// Now attempt to delete the original item using a transaction
468+
TransactWriteItemsEnhancedRequest request =
469+
TransactWriteItemsEnhancedRequest.builder()
470+
.addDeleteItem(recordWithVersionMappedTable, modifiedItem, false)
471+
.build();
472+
473+
enhancedClient.transactWriteItems(request);
474+
475+
RecordWithVersion deletedItem = recordWithVersionMappedTable.getItem(r -> r.key(recordKey));
476+
assertThat(deletedItem).isNull();
477+
}
375478
}

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

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -275,25 +275,53 @@ public <T> Builder addDeleteItem(MappedTableResource<T> mappedTableResource, Key
275275
}
276276

277277
/**
278-
* Adds a primary lookup key for the item to delete, and it's associated table, to the transaction. For more information
279-
* on the delete action, see the low-level operation description in for instance
280-
* {@link DynamoDbTable#deleteItem(DeleteItemEnhancedRequest)}.
278+
* Adds a primary lookup key for the item to delete, and its associated table, to the transaction.
279+
* <p>
280+
* This method does not enable optimistic locking: the deletion will proceed regardless of the
281+
* item's version, and no version check is applied.
282+
* </p>
281283
*
282284
* @param mappedTableResource the table where the key is located
283-
* @param keyItem an item that will have its key fields used to match a record to retrieve from the database
285+
* @param keyItem an item whose key fields identify the record to delete
284286
* @param <T> the type of modelled objects in the table
285287
*
286-
* @throws TransactionCanceledException if the version of the specified object to be deleted is inconsistent with
287-
* the version persisted in the database
288-
*
289288
* @return a builder of this type
290289
*/
291290
public <T> Builder addDeleteItem(MappedTableResource<T> mappedTableResource, T keyItem) {
292-
TransactDeleteItemEnhancedRequest request =
293-
TransactDeleteItemEnhancedRequest.builder().key(mappedTableResource.keyFrom(keyItem)).build();
294-
itemSupplierList.add(() -> generateTransactDeleteItem(mappedTableResource, DeleteItemOperation.create(request),
295-
mappedTableResource.tableSchema().itemToMap(keyItem, true)));
296-
return this;
291+
return addDeleteItem(mappedTableResource, keyItem, false);
292+
}
293+
294+
/**
295+
* Overload of {@link #addDeleteItem(MappedTableResource, Object)} that adds optional optimistic locking support.
296+
* <p>
297+
* When {@code useOptimisticLocking} is {@code true}, the delete operation will include a condition
298+
* expression to verify the item's version attribute matches the expected value. If the version in
299+
* the database differs, a {@link TransactionCanceledException} will be thrown.
300+
* Otherwise, this method behaves exactly like the original overload and deletes
301+
* the item without any version check.
302+
* </p>
303+
*
304+
* @param mappedTableResource the table where the key is located
305+
* @param keyItem an item whose key fields identify the record to delete
306+
* @param useOptimisticLocking whether to enable optimistic locking (version check) for deletion
307+
* @param <T> the type of modelled objects in the table
308+
*
309+
* @throws TransactionCanceledException if {@code useOptimisticLocking} is {@code true} and the
310+
* version of the specified object does not match the version
311+
* persisted in the database
312+
*
313+
* @return a builder of this type
314+
*/
315+
public <T> Builder addDeleteItem(MappedTableResource<T> mappedTableResource, T keyItem, boolean useOptimisticLocking) {
316+
if (useOptimisticLocking) {
317+
TransactDeleteItemEnhancedRequest request =
318+
TransactDeleteItemEnhancedRequest.builder().key(mappedTableResource.keyFrom(keyItem)).build();
319+
itemSupplierList.add(() -> generateTransactDeleteItem(mappedTableResource, DeleteItemOperation.create(request),
320+
mappedTableResource.tableSchema().itemToMap(keyItem, true)));
321+
return this;
322+
}
323+
324+
return addDeleteItem(mappedTableResource, mappedTableResource.keyFrom(keyItem));
297325
}
298326

299327
/**

0 commit comments

Comments
 (0)