Use extensions
The DynamoDB Enhanced Client API supports plugin extensions that provide functionality beyond mapping
operations. Extensions have two hook methods, beforeWrite()
and
afterRead()
. beforeWrite()
modifies a write operation before it
happens, and the afterRead()
method modifies the results of a read operation after
it happens. Because some operations (such as item updates) perform both a write and then a read,
both hook methods are called.
Extensions are loaded in the order that they are specified 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.
The enhanced client API comes with a set of plugin extensions that are located in the
extensions
package. By default, the enhanced client loads the VersionedRecordExtension
and the AtomicCounterExtension
. You can override the default behavior with the
enhance client builder and load any extension. You can also specify none if you don't want the
default extensions.
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.
In the following example, a custom extension named verifyChecksumExtension
is
loaded after the VersionedRecordExtension
, which is usually loaded by default by
itself. The AtomicCounterExtension
is not loaded in this example.
DynamoDbEnhancedClientExtension versionedRecordExtension = VersionedRecordExtension.builder().build(); DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder() .dynamoDbClient(dynamoDbClient) .extensions(versionedRecordExtension, verifyChecksumExtension) .build();
VersionedRecordExtension
The VersionedRecordExtension
is loaded by default and will increment and
track an item version number as items are written to the database. A condition will be 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. This behavior effectively
provides optimistic locking for item updates. If another process updates an item between the
time the first process has read the item and is writing an update to it, the write will
fail.
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())
AtomicCounterExtension
The AtomicCounterExtension
is loaded by default and increments a tagged
numerical attribute each time a record is written to the database. Start and increment values
can be specified. If no values are specified, the start value is set to 0 and the attribute's
value increments by 1.
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))
AutoGeneratedTimestampRecordExtension
The AutoGeneratedTimestampRecordExtension
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. Therefore, you need to specify it as a custom extension when you build the enhanced client as shown in the first example in this topic.
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 extensions 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())
AutoGeneratedUuidExtension
You can generate a unique UUID (Universally Unique Identifier) for an attribute when a new
record is written to the database by using the AutoGeneratedUuidExtension
java.lang.String
.
Since the Java SDK does not load this extension by default, you need to specify it as a custom extension when you build the enhanced client as shown in the first example in this topic.
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
@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))
Custom extensions
The following custom extension class shows a beforeWrite()
method that uses
an update expression. After comment line 2, we create a SetAction
to set the
registrationDate
attribute if the item in the database doesn't already have a
registrationDate
attribute. Whenever a Customer
object is updated,
the extension makes sure that a registrationDate
is set.
public final class CustomExtension implements DynamoDbEnhancedClientExtension { // 1. In a custom extension, use an UpdateExpression to define what action to take before // an item is updated. @Override public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite context) { if ( context.operationContext().tableName().equals("Customer") && context.operationName().equals(OperationName.UPDATE_ITEM)) { return WriteModification.builder() .updateExpression(createUpdateExpression()) .build(); } return WriteModification.builder().build(); // Return an "empty" WriteModification instance if the extension should not be applied. // In this case, if the code is not updating an item on the Customer table. } private static UpdateExpression createUpdateExpression() { // 2. Use a SetAction, a subclass of UpdateAction, to provide the values in the update. SetAction setAction = SetAction.builder() .path("registrationDate") .value("if_not_exists(registrationDate, :regValue)") .putExpressionValue(":regValue", AttributeValue.fromS(Instant.now().toString())) .build(); // 3. Build the UpdateExpression with one or more UpdateAction. return UpdateExpression.builder() .addAction(setAction) .build(); } }