Changes in the DynamoDB mapping APIs between version 1 and version 2 of the SDK for Java - Amazon SDK for Java 2.x
Services or capabilities described in Amazon Web Services documentation might vary by Region. To see the differences applicable to the China Regions, see Getting Started with Amazon Web Services in China (PDF).

Changes in the DynamoDB mapping APIs between version 1 and version 2 of the SDK for Java

Create a client

Use case V1 V2

Normal instantiation

AmazonDynamoDB standardClient = AmazonDynamoDBClientBuilder.standard() .withCredentials(credentialsProvider) .withRegion(Regions.US_EAST_1) .build(); DynamoDBMapper mapper = new DynamoDBMapper(standardClient);
DynamoDbClient standardClient = DynamoDbClient.builder() .credentialsProvider(ProfileCredentialsProvider.create()) .region(Region.US_EAST_1) .build(); DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder() .dynamoDbClient(standardClient) .build();

Minimal instantiation

AmazonDynamoDB standardClient = AmazonDynamoDBClientBuilder.standard(); DynamoDBMapper mapper = new DynamoDBMapper(standardClient);
DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.create();

With attribute transformer*

DynamoDBMapper mapper = new DynamoDBMapper(standardClient, attributeTransformerInstance);
DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder() .dynamoDbClient(standardClient) .extensions(extensionAInstance, extensionBInstance) .build();

*Extensions in V2 correspond roughly to attribute transformers in V1. The Use extensions to customize DynamoDB Enhanced Client operations section contains more information on extensions in V2.

Establish mapping to DynamoDB table/index

In V1, you specify a DynamoDB table name through a bean annotation. In V2, a factory method, table(), produces an instance of DynamoDbTable that represents the remote DynamoDB table. The first parameter of the table() method is the DynamoDB table name.

Use case V1 V2

Map the Java POJO class to the DynamoDB table

@DynamoDBTable(tableName ="Customer") public class Customer { ... }
DynamoDbTable<Customer> customerTable = enhancedClient.table("Customer", TableSchema.fromBean(Customer.class));

Map to a DynamoDB secondary index

  1. Define a POJO class that represents the index.

    • Annotate class with the @DynamoDBTable supplying the name of the table that has the index.

    • Annotate properties with @DynamoDBIndexHashKey and optionally @DynamoDBIndexRangeKey.

  2. Create a query expression.

  3. Query using reference to the POJO class that represents the index. For example

    mapper.query(IdEmailIndex.class, queryExpression)

    where IdEmailIndex is the mapping class for the index.

The section in the DynamoDB Developer Guide that discusses the V1 query method shows a complete example.

  1. Annotate attributes of a POJO class with @DynamoDbSecondaryPartitionKey (for a GSI) and @DynamoDbSecondarySortKey (for and GSI or LSI). For example,

    @DynamoDbSecondarySortKey(indexNames = "IdEmailIndex") public String getEmail() { return this.email; }
  2. Retrieve a reference to the index. For example,

    DynamoDbIndex<Customer> customerIndex = customerTable.index("IdEmailIndex");
  3. Query the index.

The Use secondary indices section in this guide provides more information.

Table operations

This section describes operation APIs that differ between V1 and V2 for most standard use cases.

In V2, all operations that involve a single table are called on the DynamoDbTable instance, not on the enhanced client. The enhanced client contains methods that can target multiple tables.

In the table named Table operations below, a POJO instance is referred to as item or as a specific type such as customer1. For the V2 examples the instances named, table is the result of previously calling enhancedClient.table() that returns a reference to the DynamoDbTable instance.

Note that most V2 operations can be called with a fluent consumer pattern even when not shown. For example,

Customer customer = table.getItem(r → r.key(key)); or Customer customer = table.getItem(r → r.key(k -> k.partitionValue("id").sortValue("email")))

For V1 operations, Table operations (below) contains some of the commonly used forms and not all overloaded forms. For example, the load() method has the following overloads:

mapper.load(Customer.class, hashKey) mapper.load(Customer.class, hashKey, rangeKey) mapper.load(Customer.class, hashKey, config) mapper.load(Customer.class, hashKey, rangeKey, config) mapper.load(item) mapper.load(item, config)

Table operations (below) shows the commonly used forms:

mapper.load(item) mapper.load(item, config)
Table operations
Use case V1 V2

Write a Java POJO to a DynamoDB table

DynamoDB operation: PutItem, UpdateItem

mapper.save(item) mapper.save(item, config) mapper.save(item, saveExpression, config)

In V1, DynamoDBMapperConfig.SaveBehavior and annotations determines which low-level DynamoDB method will be called. In general, UpdateItem is called except when using SaveBehavior.CLOBBER and SaveBehavior.PUT. Auto-generated keys are a special use case, and occasionally both PutItem and UpdateItem are used.

table.putItem(putItemRequest) table.putItem(item) table.putItemWithResponse(item) //Returns metadata. updateItem(updateItemRequest) table.updateItem(item) table.updateItemWithResponse(item) //Returns metadata.

Read an item from a DynamoDB table to a Java POJO

DynamoDB operation: GetItem

mapper.load(item) mapper.load(item, config)
table.getItem(getItemRequest) table.getItem(item) table.getItem(key) table.getItemWithResponse(key) //Returns POJO with metadata.

Delete an item from a DynamoDB table

DynamoDB operation: DeleteItem

mapper.delete(item, deleteExpression, config)
table.deleteItem(deleteItemRequest) table.deleteItem(item) table.deleteItem(key)

Query a DynamoDB table or secondary index and return a paginated list

DynamoDB operation: Query

mapper.query(Customer.class, queryExpression) mapper.query(Customer.class, queryExpression, mapperConfig)
table.query(queryRequest) table.query(queryConditional)

Use the returned PageIterable.stream() (lazy loading) for sync responses and PagePublisher.subscribe() for async responses

Query a DynamoDB table or secondary index and return a list

DynamoDB operation: Query

mapper.queryPage(Customer.class, queryExpression) mapper.queryPage(Customer.class, queryExpression, mapperConfig)
table.query(queryRequest) table.query(queryConditional)

Use the returned PageIterable.items() (lazy loading) for sync responses and PagePublisher.items.subscribe() for async responses

Scan a DynamoDB table or secondary index and return a paginated list

DynamoDB operation: Scan

mapper.scan(Customer.class, scanExpression) mapper.scan(Customer.class, scanExpression, mapperConfig)
table.scan() table.scan(scanRequest)

Use the returned PageIterable.stream() (lazy loading) for sync responses and PagePublisher.subscribe() for async responses

Scan a DynamoDB table or secondary index and return a list

DynamoDB operation: Scan

mapper.scanPage(Customer.class, scanExpression) mapper.scanPage(Customer.class, scanExpression, mapperConfig)
table.scan() table.scan(scanRequest)

Use the returned PageIterable.items() (lazy loading) for sync responses and PagePublisher.items.subscribe() for async responses

Read multiple items from multiple tables in a batch

DynamoDB operation: BatchGetItem

mapper.batchLoad(Arrays.asList(customer1, customer2, book1)) mapper.batchLoad(itemsToGet) // itemsToGet: Map<Class<?>, List<KeyPair>>
enhancedClient.batchGetItem(batchGetItemRequest) enhancedClient.batchGetItem(r -> r.readBatches( ReadBatch.builder(Record1.class) .mappedTableResource(mappedTable1) .addGetItem(i -> i.key(k -> k.partitionValue(0))) .build(), ReadBatch.builder(Record2.class) .mappedTableResource(mappedTable2) .addGetItem(i -> i.key(k -> k.partitionValue(0))) .build())) // Iterate over pages with lazy loading or over all items from the same table.

Write multiple items to multiple tables in a batch

DynamoDB operation: BatchWriteItem

mapper.batchSave(Arrays.asList(customer1, customer2, book1))
enhancedClient.batchWriteItem(batchWriteItemRequest) enhancedClient.batchWriteItem(r -> r.writeBatches( WriteBatch.builder(Record1.class) .mappedTableResource(mappedTable1) .addPutItem(item1) .build(), WriteBatch.builder(Record2.class) .mappedTableResource(mappedTable2) .addPutItem(item2) .build()))

Delete multiple items from multiple tables in a batch

DynamoDB operation: BatchWriteItem

mapper.batchDelete(Arrays.asList(customer1, customer2, book1))
enhancedClient.batchWriteItem(r -> r.writeBatches( WriteBatch.builder(Record1.class) .mappedTableResource(mappedTable1) .addDeleteItem(item1key) .build(), WriteBatch.builder(Record2.class) .mappedTableResource(mappedTable2) .addDeleteItem(item2key) .build()))

Write/delete multiple items in a batch

DynamoDB operation: BatchWriteItem

mapper.batchWrite(Arrays.asList(customer1, book1), Arrays.asList(customer2))
enhancedClient.batchWriteItem(r -> r.writeBatches( WriteBatch.builder(Record1.class) .mappedTableResource(mappedTable1) .addPutItem(item1) .build(), WriteBatch.builder(Record2.class) .mappedTableResource(mappedTable2) .addDeleteItem(item2key) .build()))

Carry out a transactional write

DynamoDB operation: TransactWriteItems

mapper.transactionWrite(transactionWriteRequest)
enhancedClient.transactWriteItems(transasctWriteItemsRequest)

Carry out a transactional read

DynamoDB operation: TransactGetItems

mapper.transactionLoad(transactionLoadRequest)
enhancedClient.transactGetItems(transactGetItemsRequest)

Get a count of matching items of a query

DynamoDB operation: Query with Select.COUNT

mapper.count(Customer.class, queryExpression)
// Get the count from query results. PageIterable<Customer> pageIterable = customerTable.query(QueryEnhancedRequest.builder() .queryConditional(queryConditional) .select(Select.COUNT) .build()); Iterator<Page<Customer>> iterator = pageIterable.iterator(); Page<Customer> page = iterator.next(); int count = page.count(); // For a more concise approach, you can chain the method calls: int count = customerTable.query(QueryEnhancedRequest.builder() .queryConditional(queryConditional) .select(Select.COUNT) .build()) .iterator().next().count();

Get a count of matching items of a scan

DynamoDB operation:Scan with Select.COUNT

mapper.count(Customer.class, scanExpression)
// Get the count from scan results. PageIterable<Customer> pageIterable = customerTable.scan(ScanEnhancedRequest.builder() .filterExpression(filterExpression) .select(Select.COUNT) .build()); Iterator<Page<Customer>> iterator = pageIterable.iterator(); Page<Customer> page = iterator.next(); int count = page.count(); // For a more concise approach, you can chain the method calls: int count = customerTable.scan(ScanEnhancedRequest.builder() .filterExpression(filterExpression) .select(Select.COUNT) .build()) .iterator().next().count();

Create a table in DynamoDB corresponding to the POJO class

DynamoDB operation: CreateTable

mapper.generateCreateTableRequest(Customer.class)

The previous statement generates a low-level create table request; users must call createTable on the DynamoDB client.

table.createTable(createTableRequest) table.createTable(r -> r.provisionedThroughput(defaultThroughput()) .globalSecondaryIndices( EnhancedGlobalSecondaryIndex.builder() .indexName("gsi_1") .projection(p -> p.projectionType(ProjectionType.ALL)) .provisionedThroughput(defaultThroughput()) .build()));

Perform a parallel scan in DynamoDB

DynamoDB operation: Scan with Segment and TotalSegments parameters

mapper.parallelScan(Customer.class, scanExpression, numTotalSegments)

Users are required to handle the worker threads and call scan for each segment:

table.scan(r -> r.segment(0).totalSegments(5))

Integrate Amazon S3 with DynamoDB to store intelligent S3 links

mapper.createS3Link(bucket, key) mapper.getS3ClientCache()

Not supported because it couples Amazon S3 and DynamoDB.

Map classes and properties

In both V1 and V2, you map classes to tables using bean-style annotations. V2 also offers other ways to define schemas for specific use cases, such as working with immutable classes.

Bean annotations

The following table shows the equivalent bean annotations for a specific use case that are used in V1 and V2. A Customer class scenario is used to illustrate parameters.

Annotations—as well as classes and enumerations—in V2 follow camel case convention and use 'DynamoDb', not 'DynamoDB'.

Use case V1 V2
Map class to table
@DynamoDBTable (tableName ="CustomerTable")
@DynamoDbBean @DynamoDbBean(converterProviders = {...})
The table name is defined when calling the DynamoDbEnhancedClient#table() method.
Designate a class member as a table attribute
@DynamoDBAttribute(attributeName = "customerName")
@DynamoDbAttribute("customerName")
Designate a class member is a hash/partition key
@DynamoDBHashKey
@DynamoDbPartitionKey
Designate a class member is a range/sort key
@DynamoDBRangeKey
@DynamoDbSortKey
Designate a class member is a secondary index hash/partition key
@DynamoDBIndexHashKey
@DynamoDbSecondaryPartitionKey
Designate a class member is a secondary index range/sort key
@DynamoDBIndexRangeKey
@DynamoDbSecondarySortKey
Ignore this class member when mapping to a table
@DynamoDBIgnore
@DynamoDbIgnore
Designate a class member as an auto-generated UUID key attribute
@DynamoDBAutoGeneratedKey
@DynamoDbAutoGeneratedUuid

The extension that provides this is not loaded by default; you must add the extension to client builder.

Designate a class member as an auto-generated timestamp attribute
@DynamoDBAutoGeneratedTimestamp
@DynamoDbAutoGeneratedTimestampAttribute

The extension that provides this is not loaded by default; you must add the extension to client builder.

Designate a class member as an auto-incremented version attribute
@DynamoDBVersionAttribute
@DynamoDbVersionAttribute

The extension that provides this is auto-loaded.

Designate a class member as requiring a custom conversion
@DynamoDBTypeConverted
@DynamoDbConvertedBy
Designate a class member to be stored as a different attribute type
@DynamoDBTyped(<DynamoDBAttributeType>)

Use an AttributeConverter implementation. V2 provides many built-in converters for common Java types. You can also implement your own custom AttributeConverter or AttributeConverterProvider. See Control attribute conversion in this guide.

Designate a class that can be serialized to a DynamoDB document (JSON-style document) or sub-document
@DynamoDBDocument
Use the Enhanced Document API. See the following resources:

V2 additional annotations

Use case V1 V2
Designate a class member not to be stored as a NULL attribute if the Java value is null N/A
@DynamoDbIgnoreNulls
Designate a class member to be an empty object if all attributes are null N/A
@DynamoDbPreserveEmptyObject
Designate special update action for a class member N/A
@DynamoDbUpdateBehavior
Designate an immutable class N/A
@DynamoDbImmutable
Designate a class member as an auto-incremented counter attribute N/A
@DynamoDbAtomicCounter

The extension that provides this functionality is auto-loaded.

Configuration

In V1, you generally control specific behaviors by using an instance of DynamoDBMapperConfig. You can supply the configuration object either when you create the mapper or when you make a request. In V2, configuration is specific to the request object for the operation.

Use case V1 Default in V1 V2
DynamoDBMapperConfig.builder()
Batch load/write retry strategy
.withBatchLoadRetryStrategy(loadRetryStrategy)
.withBatchWriteRetryStrategy(writeRetryStrategy)
retry failed items Configure the retry strategy on the underlying DynamoDBClient. See Configure retry behavior in the Amazon SDK for Java 2.x in this guide.
Consistent reads
.withConsistentReads(CONSISTENT)
EVENTUAL By default, consistent reads is false for read operations. Override with .consistentRead(true) on the request object.
Conversion schema with sets of marshallers/unmarshallers
.withConversionSchema(conversionSchema)

Static implementations provide backwards compatibility with older versions.

V2_COMPATIBLE Not applicable. This is a legacy feature that refers to how the earliest versions of DynamoDB (V1) stored data types, and this behavior will not be preserved in the enhanced client. An example of behavior in DynamoDB V1 is storing booleans as Number instead of as Boolean.
Table names
.withObjectTableNameResolver() .withTableNameOverride() .withTableNameResolver()

Static implementations provide backwards compatibility with older versions

use annotation or guess from class

The table name is defined when calling the DynamoDbEnhancedClient#table() method.

Pagination load strategy
.withPaginationLoadingStrategy(strategy)

Options are: LAZY_LOADING, EAGER_LOADING, or ITERATION_ONLY

LAZY_LOADING
  • Iteration only is the default. The other V1 options are not supported.

  • You can implement the equivalent of eager loading in V2 with the following:

    List<Customer> allItems = customerTable.scan().items().stream().collect(Collectors.toList());
  • For lazy loading, you must implement your own caching logic for accessed items.

Request metric collection
.withRequestMetricCollector(collector)
null Use metricPublisher() in ClientOverrideConfiguration when building the standard DynamoDB client.
Save behavior
.withSaveBehavior(SaveBehavior.CLOBBER)

Options are UPDATE, CLOBBER, PUT, APPEND_SET, or UPDATE_SKIP_NULL_ATTRIBUTES.

UPDATE

In V2, you call putItem() or updateItem() explicitly.

CLOBBER or PUT: Corresponding action in v 2 is calling putItem(). There is no specific CLOBBER configuration.

UPDATE: Corresponds to updateItem()

UPDATE_SKIP_NULL_ATTRIBUTES: Corresponds to updateItem(). Control update behavior with the request setting ignoreNulls and the annotation/tag DynamoDbUpdateBehavior.

APPEND_SET: Not supported

Type converter factory
.withTypeConverterFactory(typeConverterFactory)
standard type converters

Set on the bean by using

@DynamoDbBean(converterProviders = {ConverterProvider.class, DefaultAttributeConverterProvider.class})

Per-operation configuration

In V1, some operations, such as query(), are highly configurable through an “expression” object submitted to the operation. For example:

DynamoDBQueryExpression<Customer> emailBwQueryExpr = new DynamoDBQueryExpression<Customer>() .withRangeKeyCondition("Email", new Condition() .withComparisonOperator(ComparisonOperator.BEGINS_WITH) .withAttributeValueList( new AttributeValue().withS("my"))); mapper.query(Customer.class, emailBwQueryExpr);

In V2, instead of using a configuration object, you set parameters on the request object by using a builder. For example:

QueryEnhancedRequest emailBw = QueryEnhancedRequest.builder() .queryConditional(QueryConditional .sortBeginsWith(kb -> kb .sortValue("my"))).build(); customerTable.query(emailBw);

Conditionals

In V2, conditional and filtering expressions are expressed using an Expression object, which encapsulates the condition and the mapping of names and filters.

Use case Operations V1 V2
Expected attribute conditions save(), delete(), query(), scan()
new DynamoDBSaveExpression() .withExpected(Collections.singletonMap( "otherAttribute", new ExpectedAttributeValue(false))) .withConditionalOperator(ConditionalOperator.AND);
Deprecated; use ConditionExpression instead.
Condition expression delete()
deleteExpression.setConditionExpression("zipcode = :zipcode") deleteExpression.setExpressionAttributeValues(...)
Expression conditionExpression = Expression.builder() .expression("#key = :value OR #key1 = :value1") .putExpressionName("#key", "attribute") .putExpressionName("#key1", "attribute3") .putExpressionValue(":value", AttributeValues.stringValue("wrong")) .putExpressionValue(":value1", AttributeValues.stringValue("three")) .build(); DeleteItemEnhancedRequest request = DeleteItemEnhancedRequest.builder() .conditionExpression(conditionExpression).build();
Filter expression query(), scan()
scanExpression .withFilterExpression("#statename = :state") .withExpressionAttributeValues(attributeValueMapBuilder.build()) .withExpressionAttributeNames(attributeNameMapBuilder.build())
Map<String, AttributeValue> values = singletonMap(":key", stringValue("value")); Expression filterExpression = Expression.builder() .expression("name = :key") .expressionValues(values) .build(); QueryEnhancedRequest request = QueryEnhancedRequest.builder() .filterExpression(filterExpression).build();
Condition expression for query query()
queryExpression.withKeyConditionExpression()
QueryConditional keyEqual = QueryConditional.keyEqualTo(b -> b .partitionValue("movie01")); QueryEnhancedRequest tableQuery = QueryEnhancedRequest.builder() .queryConditional(keyEqual) .build();

Type conversion

Default converters

In V2, the SDK provides a set of default converters for all common types. You can change type converters both at the overall provider level as well as for a single attribute. You can find a list of the available converters in the AttributeConverter API reference.

Set a custom converter for an attribute

In V1, you can annotate a getter method with @DynamoDBTypeConverted to specify the class that converts between the Java attribute type and a DynamoDB attribute type. For instance, a CurrencyFormatConverter that converts between a Java Currency type and DynamoDB String can be applied as shown in the following snippet.

@DynamoDBTypeConverted(converter = CurrencyFormatConverter.class) public Currency getCurrency() { return currency; }

The V2 equivalent of the previous snippet is shown below.

@DynamoDbConvertedBy(CurrencyFormatConverter.class) public Currency getCurrency() { return currency; }
Note

In V1, you can apply the annotation to the attribute itself , a type or a user-defined annotation, V2 supports applying the annotation it only to the getter.

Add a type converter factory or provider

In V1, you can provide your own set of type converters, or override the types you care about by adding a type converter factory to the configuration. The type converter factory extends DynamoDBTypeConverterFactory, and overrides are done by getting a reference to the default set and extending it. The following snippet demonstrates how to do this.

DynamoDBTypeConverterFactory typeConverterFactory = DynamoDBTypeConverterFactory.standard().override() .with(String.class, CustomBoolean.class, new DynamoDBTypeConverter<String, CustomBoolean>() { @Override public String convert(CustomBoolean bool) { return String.valueOf(bool.getValue()); } @Override public CustomBoolean unconvert(String string) { return new CustomBoolean(Boolean.valueOf(string)); }}).build(); DynamoDBMapperConfig config = DynamoDBMapperConfig.builder() .withTypeConverterFactory(typeConverterFactory) .build(); DynamoDBMapper mapperWithTypeConverterFactory = new DynamoDBMapper(dynamo, config);

V2 provides similar functionality through the @DynamoDbBean annotation. You can provide a single AttributeConverterProvider or a chain of ordered AttributeConverterProviders. Note that if you supply your own chain of attribute converter providers, you will override the default converter provider and must include it in the chain to use its attribute converters.

@DynamoDbBean(converterProviders = { ConverterProvider1.class, ConverterProvider2.class, DefaultAttributeConverterProvider.class}) public class Customer { ... }

The section on attribute conversion in this guide contains a complete example for V2.