The DynamoDB Enhanced Client, part of the AWS V2 SDK, offers a straightforward way to map client-side classes to DynamoDB tables and perform CRUD operations.
For example, instances of a Customer
class can map to a row in a customers
DynamoDB table.
The enhanced client supports mappings for immutable classes, which is possible with the following annotations (starting in immutables version 2.10):
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable;
@Value.Immutable
@DynamoDbImmutable(builder = ImmutableCustomer.Builder.class)
@Value.Style(
from = "", // Omit the from(*) methods so that they aren't interpreted as table attributes
defaults = @Value.Immutable(copy = false), // Omit the copy(*) methods so that they aren't interpreted as table attribute
builtinContainerAttributes = false, // Omit the add(*) methods so that they aren't interpreted as table attributes
passAnnotations = {
// Copy all Enhanced Client annotations to the immutable class
DynamoDbImmutable.class,
DynamoDbAttribute.class,
DynamoDbConvertedBy.class,
DynamoDbFlatten.class,
DynamoDbIgnore.class,
DynamoDbIgnoreNulls.class,
DynamoDbPartitionKey.class,
DynamoDbPreserveEmptyObject.class,
DynamoDbSecondaryPartitionKey.class,
DynamoDbSecondarySortKey.class,
DynamoDbSortKey.class,
DynamoDbUpdateBehavior.class,
DynamoDbAtomicCounter.class,
DynamoDbAutoGeneratedTimestampAttribute.class,
DynamoDbVersionAttribute.class,
}
)
public interface Customer {
// Partition key attribute named "customerId"
@DynamoDbPartitionKey
String getCustomerId();
//Sort key attribute named "email"
@DynamoDbSortKey
String getEmail();
//"name" attribute
String getName();
//An optional "occupation" attribute. The @Nullable annotation is used because the Enhanced Client does not support java.util.Optional
@Nullable
String getOccupation();
}
Then to persist the item:
DynamoDbEnhancedClient client = ...
//Initialize the schema and table mapping
TableSchema<ImmutableCustomer> customerSchema = TableSchema.fromImmutableClass(ImmutableCustomer.class);
DynamoDbTable<ImmutableCustomer> customerTable = client.table("customers", customerSchema);
// Build and persist the customer
ImmutableCustomer customer = ImmutableCustomer.builder()
.customerId("customer123")
.email("example@email.com")
.name("John")
.build();
customerTable.putItem(customer);
// Retrieve the customer
Key lookupKey = Key.builder().partitionValue("customer123").sortValue("example@email.com").build();
ImmutableCustomer retrievedCustomer = customerTable.getItem(lookupKey);
When updating an item, the Enhanced Client’s interface takes an instance of the table mapping class as input, but allows you to set attribute values to null
to indicate they shouldn’t be modified. For example, you may want to atomically increment a counter attribute without overwriting other attributes with potentially stale values.
Ideally partial updates are possible without making all attributes optional in the table mapping class. This is possible by extending the mapping class and disabling immutables validation of required fields:
@Value.Immutable
@SuppressWarnings("immutables:subtype")
@DynamoDbImmutable(builder = ImmutablePartialCustomer.Builder.class)
@Value.Style(
// Disable required field validation
validationMethod = ValidationMethod.NONE,
from = "",
builtinContainerAttributes = false,
defaults = @Value.Immutable(copy = false),
passAnnotations = {
// Same annotations as in Customer
}
)
public interface PartialCustomer extends Customer {
}
And then to make a partial update:
// Initialize the schema and table mappings for the partial class items. The same "customers" table is used as the non-partial mapping.
TableSchema<ImmutablePartialCustomer> partialCustomerSchema = TableSchema.fromImmutableClass(ImmutablePartialCustomer.class);
DynamoDbTable<ImmutablePartialCustomer> partialCustomerTable = client.table("customers", partialCustomerSchema);
// Update just the occupation attribute, not the required "name" attribute
ImmutablePartialCustomer partialUpdate = ImmutablePartialCustomer.builder()
.customerId("customer123")
.email("example@email.com")
.occupation("software developer")
.build();
// ignoreNulls(true) only updates non-null attributes
partialCustomerTable.updateItem(request -> request.ignoreNulls(true).item(partialUpdate));
Custom annotations can be created to re-use the above styles across tables:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
@Value.Style(
from = "",
builtinContainerAttributes = false,
defaults = @Value.Immutable(copy = false),
passAnnotations = {
DynamoDbImmutable.class,
DynamoDbAttribute.class,
DynamoDbConvertedBy.class,
DynamoDbFlatten.class,
DynamoDbIgnore.class,
DynamoDbIgnoreNulls.class,
DynamoDbPartitionKey.class,
DynamoDbPreserveEmptyObject.class,
DynamoDbSecondaryPartitionKey.class,
DynamoDbSecondarySortKey.class,
DynamoDbSortKey.class,
DynamoDbUpdateBehavior.class,
DynamoDbAtomicCounter.class,
DynamoDbAutoGeneratedTimestampAttribute.class,
DynamoDbVersionAttribute.class,
}
)
public @interface ImmutablesDynamoDBStyle {}
@Value.Immutable
@ImmutablesDynamoDBStyle
@DynamoDbImmutable(builder = ImmutableCustomer.Builder.class)
public interface Customer { ... }
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
@Value.Style(
validationMethod = ValidationMethod.NONE,
from = "",
builtinContainerAttributes = false,
defaults = @Value.Immutable(copy = false),
passAnnotations = {
DynamoDbImmutable.class,
DynamoDbAttribute.class,
DynamoDbConvertedBy.class,
DynamoDbFlatten.class,
DynamoDbIgnore.class,
DynamoDbIgnoreNulls.class,
DynamoDbPartitionKey.class,
DynamoDbPreserveEmptyObject.class,
DynamoDbSecondaryPartitionKey.class,
DynamoDbSecondarySortKey.class,
DynamoDbSortKey.class,
DynamoDbUpdateBehavior.class,
DynamoDbAtomicCounter.class,
DynamoDbAutoGeneratedTimestampAttribute.class,
DynamoDbVersionAttribute.class,
}
)
public @interface ImmutablesDynamoDBPartialStyle {}
@Value.Immutable
@ImmutablesDynamoDBPartialStyle
@SuppressWarnings("immutables:subtype")
@DynamoDbImmutable(builder = ImmutablePartialCustomer.Builder.class)
public interface PartialCustomer extends Customer { }
A guide for V1 of the SDK using DynamoDbMapper
can be found here