Use extensions to customize DynamoDB Enhanced Client operations - AWS SDK for Java 2.x

Use extensions to customize DynamoDB Enhanced Client operations

The DynamoDB Enhanced Client API supports plugin extensions that provide functionality beyond mapping operations. Extensions use two hook methods to modify data during read and write operations:

  • beforeWrite() - Modifies a write operation before it happens

  • afterRead() - Modifies the results of a read operation after it happens

Some operations (such as item updates) perform both a write and then a read, so both hook methods are called.

How extensions are loaded

Extensions are loaded in the order that you specify in the enhanced client builder. The load order can be important because one extension can act on values that have been transformed by a previous extension.

By default, the enhanced client loads two extensions:

You can override the default behavior with the enhanced client builder and load any extension. You can also specify none if you don't want the default extensions.

Important

If you load your own extensions, the enhanced client doesn't load any default extensions. If you want the behavior provided by either default extension, you need to explicitly add it to the list of extensions.

The following example shows how to load a custom extension named verifyChecksumExtension after the VersionedRecordExtension. The AtomicCounterExtension is not loaded in this example.

DynamoDbEnhancedClientExtension versionedRecordExtension = VersionedRecordExtension.builder().build(); DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder() .dynamoDbClient(dynamoDbClient) .extensions(versionedRecordExtension, verifyChecksumExtension) .build();

Available extension details and configuration

The following sections provide detailed information about each available extension in the SDK.

Implement optimistic locking with the VersionedRecordExtension

The VersionedRecordExtension extension provides optimistic locking by incrementing and tracking an item version number as items are written to the database. A condition is added to every write that causes the write to fail if the version number of the actual persisted item doesn't match the value that the application last read.

Configuration

To specify which attribute to use to track the item version number, tag a numeric attribute in the table schema.

The following snippet specifies that the version attribute should hold the item version number.

@DynamoDbVersionAttribute public Integer getVersion() {...}; public void setVersion(Integer version) {...};

The equivalent static table schema approach is shown in the following snippet.

.addAttribute(Integer.class, a -> a.name("version") .getter(Customer::getVersion) .setter(Customer::setVersion) // Apply the 'version' tag to the attribute. .tags(VersionedRecordExtension.AttributeTags.versionAttribute())

How it works

Optimistic locking with the VersionedRecordExtension has the following impact on these DynamoDbEnhancedClient and DynamoDbTable methods:

putItem

New items are assigned a initial version value of 0. This can be configured with @DynamoDbVersionAttribute(startAt = X).

updateItem

If you retrieve an item, update one or more of its properties, and attempt to save the changes, the operation succeeds only if the version number on the client side and the server side match.

If successful, the version number is automatically incremented by 1. This can be configured with @DynamoDbVersionAttribute(incrementBy = X).

deleteItem

The DynamoDbVersionAttribute annotation has no effect. You must add a condition expressions manually when deleting an item.

The following example adds a conditional expression to ensure that the item deleted is the item that was read. In the following example recordVersion is the bean's attribute annotated with @DynamoDbVersionAttribute.

// 1. Read the item and get its current version. Customer item = customerTable.getItem(Key.builder().partitionValue("someId").build()); // `recordVersion` is the bean's attribute that is annotated with `@DynamoDbVersionAttribute`. AttributeValue currentVersion = item.getRecordVersion(); // 2. Create conditional delete with the `currentVersion` value. DeleteItemEnhancedRequest deleteItemRequest = DeleteItemEnhancedRequest.builder() .key(KEY) .conditionExpression(Expression.builder() .expression("recordVersion = :current_version_value") .putExpressionValue(":current_version_value", currentVersion) .build()).build(); customerTable.deleteItem(deleteItemRequest);
transactWriteItems
  • addPutItem: This method has the same behavior as putItem.

  • addUpdateItem: This method has the same behavior as updateItem.

  • addDeleteItem: This method has the same behavior as deleteItem.

batchWriteItem
  • addPutItem: This method has the same behavior as putItem.

  • addDeleteItem: This method has the same behavior as deleteItem.

Note

DynamoDB global tables use a 'last writer wins' reconciliation between concurrent updates, where DynamoDB makes a best effort to determine the last writer. If you use global tables, this 'last writer wins' policy means that locking strategies may not work as expected, because all replicas will eventually converge based on the last write determined by DynamoDB.

How to disable

To disable optimistic locking, do not use the @DynamoDbVersionAttribute annotation.

Implement counters with the AtomicCounterExtension

The AtomicCounterExtension extension increments a tagged numerical attribute each time a record is written to the database. You can specify start and increment values. If no values are specified, the start value is set to 0 and the attribute's value increments by 1.

Configuration

To specify which attribute is a counter, tag an attribute of type Long in the table schema.

The following snippet shows the use of the default start and increment values for the counter attribute.

@DynamoDbAtomicCounter public Long getCounter() {...}; public void setCounter(Long counter) {...};

The static table schema approach is shown in the following snippet. The atomic counter extension uses a start value of 10 and increments the value by 5 each time the record is written.

.addAttribute(Integer.class, a -> a.name("counter") .getter(Customer::getCounter) .setter(Customer::setCounter) // Apply the 'atomicCounter' tag to the attribute with start and increment values. .tags(StaticAttributeTags.atomicCounter(10L, 5L))

Add timestamps with the AutoGeneratedTimestampRecordExtension

The AutoGeneratedTimestampRecordExtension extension automatically updates tagged attributes of type Instant with a current timestamp every time the item is successfully written to the database. This extension is not loaded by default.

Configuration

To specify which attribute to update with the current timestamp, tag the Instant attribute in the table schema.

The lastUpdate attribute is the target of the extension's behavior in the following snippet. Note the requirement that the attribute must be an Instant type.

@DynamoDbAutoGeneratedTimestampAttribute public Instant getLastUpdate() {...} public void setLastUpdate(Instant lastUpdate) {...}

The equivalent static table schema approach is shown in the following snippet.

.addAttribute(Instant.class, a -> a.name("lastUpdate") .getter(Customer::getLastUpdate) .setter(Customer::setLastUpdate) // Applying the 'autoGeneratedTimestamp' tag to the attribute. .tags(AutoGeneratedTimestampRecordExtension.AttributeTags.autoGeneratedTimestampAttribute())

Generate a UUID with the AutoGeneratedUuidExtension

The AutoGeneratedUuidExtension extension generates a unique UUID (Universally Unique Identifier) for an attribute when a new record is written to the database. Uses the Java JDK UUID.randomUUID() method and applies to attributes of type java.lang.String. This extension is not loaded by default.

Configuration

The uniqueId attribute is the target of the extension's behavior in the following snippet.

@AutoGeneratedUuidExtension public String getUniqueId() {...} public void setUniqueId(String uniqueId) {...}

The equivalent static table schema approach is shown in the following snippet.

.addAttribute(String.class, a -> a.name("uniqueId") .getter(Customer::getUniqueId) .setter(Customer::setUniqueId) // Applying the 'autoGeneratedUuid' tag to the attribute. .tags(AutoGeneratedUuidExtension.AttributeTags.autoGeneratedUuidAttribute())

If you want the extension to populate the UUID only for putItem methods and not for updateItem methods, add the update behavior annotation as shown in the following snippet.

@AutoGeneratedUuidExtension @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) public String getUniqueId() {...} public void setUniqueId(String uniqueId) {...}

If you use the static table schema approach, use the following equivalent code.

.addAttribute(String.class, a -> a.name("uniqueId") .getter(Customer::getUniqueId) .setter(Customer::setUniqueId) // Applying the 'autoGeneratedUuid' tag to the attribute. .tags(AutoGeneratedUuidExtension.AttributeTags.autoGeneratedUuidAttribute(), StaticAttributeTags.updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))