1*8a52c783SCole Faust## Overview 2*8a52c783SCole Faust 3*8a52c783SCole FaustMid-level DynamoDB mapper/abstraction for Java using the v2 AWS SDK. 4*8a52c783SCole Faust 5*8a52c783SCole Faust## Getting Started 6*8a52c783SCole FaustAll the examples below use a fictional Customer class. This class is 7*8a52c783SCole Faustcompletely made up and not part of this library. Any search or key 8*8a52c783SCole Faustvalues used are also completely arbitrary. 9*8a52c783SCole Faust 10*8a52c783SCole Faust### Initialization 11*8a52c783SCole Faust1. Create or use a java class for mapping records to and from the 12*8a52c783SCole Faust database table. At a minimum you must annotate the class so that 13*8a52c783SCole Faust it can be used as a DynamoDb bean, and also the property that 14*8a52c783SCole Faust represents the primary partition key of the table. Here's an example:- 15*8a52c783SCole Faust ```java 16*8a52c783SCole Faust @DynamoDbBean 17*8a52c783SCole Faust public class Customer { 18*8a52c783SCole Faust private String accountId; 19*8a52c783SCole Faust private int subId; // primitive types are supported 20*8a52c783SCole Faust private String name; 21*8a52c783SCole Faust private Instant createdDate; 22*8a52c783SCole Faust 23*8a52c783SCole Faust @DynamoDbPartitionKey 24*8a52c783SCole Faust public String getAccountId() { return this.accountId; } 25*8a52c783SCole Faust public void setAccountId(String accountId) { this.accountId = accountId; } 26*8a52c783SCole Faust 27*8a52c783SCole Faust @DynamoDbSortKey 28*8a52c783SCole Faust public int getSubId() { return this.subId; } 29*8a52c783SCole Faust public void setSubId(int subId) { this.subId = subId; } 30*8a52c783SCole Faust 31*8a52c783SCole Faust // Defines a GSI (customers_by_name) with a partition key of 'name' 32*8a52c783SCole Faust @DynamoDbSecondaryPartitionKey(indexNames = "customers_by_name") 33*8a52c783SCole Faust public String getName() { return this.name; } 34*8a52c783SCole Faust public void setName(String name) { this.name = name; } 35*8a52c783SCole Faust 36*8a52c783SCole Faust // Defines an LSI (customers_by_date) with a sort key of 'createdDate' and also declares the 37*8a52c783SCole Faust // same attribute as a sort key for the GSI named 'customers_by_name' 38*8a52c783SCole Faust @DynamoDbSecondarySortKey(indexNames = {"customers_by_date", "customers_by_name"}) 39*8a52c783SCole Faust public Instant getCreatedDate() { return this.createdDate; } 40*8a52c783SCole Faust public void setCreatedDate(Instant createdDate) { this.createdDate = createdDate; } 41*8a52c783SCole Faust } 42*8a52c783SCole Faust ``` 43*8a52c783SCole Faust 44*8a52c783SCole Faust2. Create a TableSchema for your class. For this example we are using a static constructor method on TableSchema that 45*8a52c783SCole Faust will scan your annotated class and infer the table structure and attributes : 46*8a52c783SCole Faust ```java 47*8a52c783SCole Faust static final TableSchema<Customer> CUSTOMER_TABLE_SCHEMA = TableSchema.fromClass(Customer.class); 48*8a52c783SCole Faust ``` 49*8a52c783SCole Faust 50*8a52c783SCole Faust If you would prefer to skip the slightly costly bean inference for a faster solution, you can instead declare your 51*8a52c783SCole Faust schema directly and let the compiler do the heavy lifting. If you do it this way, your class does not need to follow 52*8a52c783SCole Faust bean naming standards nor does it need to be annotated. This example is equivalent to the bean example : 53*8a52c783SCole Faust ```java 54*8a52c783SCole Faust static final TableSchema<Customer> CUSTOMER_TABLE_SCHEMA = 55*8a52c783SCole Faust TableSchema.builder(Customer.class) 56*8a52c783SCole Faust .newItemSupplier(Customer::new) 57*8a52c783SCole Faust .addAttribute(String.class, a -> a.name("account_id") 58*8a52c783SCole Faust .getter(Customer::getAccountId) 59*8a52c783SCole Faust .setter(Customer::setAccountId) 60*8a52c783SCole Faust .tags(primaryPartitionKey())) 61*8a52c783SCole Faust .addAttribute(Integer.class, a -> a.name("sub_id") 62*8a52c783SCole Faust .getter(Customer::getSubId) 63*8a52c783SCole Faust .setter(Customer::setSubId) 64*8a52c783SCole Faust .tags(primarySortKey())) 65*8a52c783SCole Faust .addAttribute(String.class, a -> a.name("name") 66*8a52c783SCole Faust .getter(Customer::getName) 67*8a52c783SCole Faust .setter(Customer::setName) 68*8a52c783SCole Faust .tags(secondaryPartitionKey("customers_by_name"))) 69*8a52c783SCole Faust .addAttribute(Instant.class, a -> a.name("created_date") 70*8a52c783SCole Faust .getter(Customer::getCreatedDate) 71*8a52c783SCole Faust .setter(Customer::setCreatedDate) 72*8a52c783SCole Faust .tags(secondarySortKey("customers_by_date"), 73*8a52c783SCole Faust secondarySortKey("customers_by_name"))) 74*8a52c783SCole Faust .build(); 75*8a52c783SCole Faust ``` 76*8a52c783SCole Faust 77*8a52c783SCole Faust3. Create a DynamoDbEnhancedClient object that you will use to repeatedly 78*8a52c783SCole Faust execute operations against all your tables :- 79*8a52c783SCole Faust ```java 80*8a52c783SCole Faust DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder() 81*8a52c783SCole Faust .dynamoDbClient(dynamoDbClient) 82*8a52c783SCole Faust .build(); 83*8a52c783SCole Faust ``` 84*8a52c783SCole Faust4. Create a DynamoDbTable object that you will use to repeatedly execute 85*8a52c783SCole Faust operations against a specific table :- 86*8a52c783SCole Faust ```java 87*8a52c783SCole Faust // Maps a physical table with the name 'customers_20190205' to the schema 88*8a52c783SCole Faust DynamoDbTable<Customer> customerTable = enhancedClient.table("customers_20190205", CUSTOMER_TABLE_SCHEMA); 89*8a52c783SCole Faust ``` 90*8a52c783SCole Faust 91*8a52c783SCole FaustThe name passed to the `table()` method above must match the name of a DynamoDB table if it already exists. 92*8a52c783SCole FaustThe DynamoDbTable object, customerTable, can now be used to perform the basic operations on the `customers_20190205` table. 93*8a52c783SCole FaustIf the table does not already exist, the name will be used as the DynamoDB table name on a subsequent `createTable()` method. 94*8a52c783SCole Faust 95*8a52c783SCole Faust### Common primitive operations 96*8a52c783SCole FaustThese all strongly map to the primitive DynamoDB operations they are 97*8a52c783SCole Faustnamed after. The examples below are the most simple variants of each 98*8a52c783SCole Faustoperation possible. Each operation can be further customized by passing 99*8a52c783SCole Faustin an enhanced request object. These enhanced request objects offer most 100*8a52c783SCole Faustof the features available in the low-level DynamoDB SDK client and are 101*8a52c783SCole Faustfully documented in the Javadoc of the interfaces referenced in these examples. 102*8a52c783SCole Faust 103*8a52c783SCole Faust ```java 104*8a52c783SCole Faust // CreateTable 105*8a52c783SCole Faust customerTable.createTable(); 106*8a52c783SCole Faust 107*8a52c783SCole Faust // GetItem 108*8a52c783SCole Faust Customer customer = customerTable.getItem(Key.builder().partitionValue("a123").build()); 109*8a52c783SCole Faust 110*8a52c783SCole Faust // UpdateItem 111*8a52c783SCole Faust Customer updatedCustomer = customerTable.updateItem(customer); 112*8a52c783SCole Faust 113*8a52c783SCole Faust // PutItem 114*8a52c783SCole Faust customerTable.putItem(customer); 115*8a52c783SCole Faust 116*8a52c783SCole Faust // DeleteItem 117*8a52c783SCole Faust Customer deletedCustomer = customerTable.deleteItem(Key.builder().partitionValue("a123").sortValue(456).build()); 118*8a52c783SCole Faust 119*8a52c783SCole Faust // Query 120*8a52c783SCole Faust PageIterable<Customer> customers = customerTable.query(keyEqualTo(k -> k.partitionValue("a123"))); 121*8a52c783SCole Faust 122*8a52c783SCole Faust // Scan 123*8a52c783SCole Faust PageIterable<Customer> customers = customerTable.scan(); 124*8a52c783SCole Faust 125*8a52c783SCole Faust // BatchGetItem 126*8a52c783SCole Faust BatchGetResultPageIterable batchResults = enhancedClient.batchGetItem(r -> r.addReadBatch(ReadBatch.builder(Customer.class) 127*8a52c783SCole Faust .mappedTableResource(customerTable) 128*8a52c783SCole Faust .addGetItem(key1) 129*8a52c783SCole Faust .addGetItem(key2) 130*8a52c783SCole Faust .addGetItem(key3) 131*8a52c783SCole Faust .build())); 132*8a52c783SCole Faust 133*8a52c783SCole Faust // BatchWriteItem 134*8a52c783SCole Faust batchResults = enhancedClient.batchWriteItem(r -> r.addWriteBatch(WriteBatch.builder(Customer.class) 135*8a52c783SCole Faust .mappedTableResource(customerTable) 136*8a52c783SCole Faust .addPutItem(customer) 137*8a52c783SCole Faust .addDeleteItem(key1) 138*8a52c783SCole Faust .addDeleteItem(key1) 139*8a52c783SCole Faust .build())); 140*8a52c783SCole Faust 141*8a52c783SCole Faust // TransactGetItems 142*8a52c783SCole Faust transactResults = enhancedClient.transactGetItems(r -> r.addGetItem(customerTable, key1) 143*8a52c783SCole Faust .addGetItem(customerTable, key2)); 144*8a52c783SCole Faust 145*8a52c783SCole Faust // TransactWriteItems 146*8a52c783SCole Faust enhancedClient.transactWriteItems(r -> r.addConditionCheck(customerTable, 147*8a52c783SCole Faust i -> i.key(orderKey) 148*8a52c783SCole Faust .conditionExpression(conditionExpression)) 149*8a52c783SCole Faust .addUpdateItem(customerTable, customer) 150*8a52c783SCole Faust .addDeleteItem(customerTable, key)); 151*8a52c783SCole Faust``` 152*8a52c783SCole Faust 153*8a52c783SCole Faust### Using secondary indices 154*8a52c783SCole FaustCertain operations (Query and Scan) may be executed against a secondary 155*8a52c783SCole Faustindex. Here's an example of how to do this: 156*8a52c783SCole Faust ```java 157*8a52c783SCole Faust DynamoDbIndex<Customer> customersByName = customerTable.index("customers_by_name"); 158*8a52c783SCole Faust 159*8a52c783SCole Faust SdkIterable<Page<Customer>> customersWithName = 160*8a52c783SCole Faust customersByName.query(r -> r.queryConditional(keyEqualTo(k -> k.partitionValue("Smith")))); 161*8a52c783SCole Faust 162*8a52c783SCole Faust PageIterable<Customer> pages = PageIterable.create(customersWithName); 163*8a52c783SCole Faust ``` 164*8a52c783SCole Faust 165*8a52c783SCole Faust### Working with immutable data classes 166*8a52c783SCole FaustIt is possible to have the DynamoDB Enhanced Client map directly to and from immutable data classes in Java. An 167*8a52c783SCole Faustimmutable class is expected to only have getters and will also be associated with a separate builder class that 168*8a52c783SCole Faustis used to construct instances of the immutable data class. The DynamoDB annotation style for immutable classes is 169*8a52c783SCole Faustvery similar to bean classes : 170*8a52c783SCole Faust 171*8a52c783SCole Faust```java 172*8a52c783SCole Faust@DynamoDbImmutable(builder = Customer.Builder.class) 173*8a52c783SCole Faustpublic class Customer { 174*8a52c783SCole Faust private final String accountId; 175*8a52c783SCole Faust private final int subId; 176*8a52c783SCole Faust private final String name; 177*8a52c783SCole Faust private final Instant createdDate; 178*8a52c783SCole Faust 179*8a52c783SCole Faust private Customer(Builder b) { 180*8a52c783SCole Faust this.accountId = b.accountId; 181*8a52c783SCole Faust this.subId = b.subId; 182*8a52c783SCole Faust this.name = b.name; 183*8a52c783SCole Faust this.createdDate = b.createdDate; 184*8a52c783SCole Faust } 185*8a52c783SCole Faust 186*8a52c783SCole Faust // This method will be automatically discovered and used by the TableSchema 187*8a52c783SCole Faust public static Builder builder() { return new Builder(); } 188*8a52c783SCole Faust 189*8a52c783SCole Faust @DynamoDbPartitionKey 190*8a52c783SCole Faust public String accountId() { return this.accountId; } 191*8a52c783SCole Faust 192*8a52c783SCole Faust @DynamoDbSortKey 193*8a52c783SCole Faust public int subId() { return this.subId; } 194*8a52c783SCole Faust 195*8a52c783SCole Faust @DynamoDbSecondaryPartitionKey(indexNames = "customers_by_name") 196*8a52c783SCole Faust public String name() { return this.name; } 197*8a52c783SCole Faust 198*8a52c783SCole Faust @DynamoDbSecondarySortKey(indexNames = {"customers_by_date", "customers_by_name"}) 199*8a52c783SCole Faust public Instant createdDate() { return this.createdDate; } 200*8a52c783SCole Faust 201*8a52c783SCole Faust public static final class Builder { 202*8a52c783SCole Faust private String accountId; 203*8a52c783SCole Faust private int subId; 204*8a52c783SCole Faust private String name; 205*8a52c783SCole Faust private Instant createdDate; 206*8a52c783SCole Faust 207*8a52c783SCole Faust private Builder() {} 208*8a52c783SCole Faust 209*8a52c783SCole Faust public Builder accountId(String accountId) { this.accountId = accountId; return this; } 210*8a52c783SCole Faust public Builder subId(int subId) { this.subId = subId; return this; } 211*8a52c783SCole Faust public Builder name(String name) { this.name = name; return this; } 212*8a52c783SCole Faust public Builder createdDate(Instant createdDate) { this.createdDate = createdDate; return this; } 213*8a52c783SCole Faust 214*8a52c783SCole Faust // This method will be automatically discovered and used by the TableSchema 215*8a52c783SCole Faust public Customer build() { return new Customer(this); } 216*8a52c783SCole Faust } 217*8a52c783SCole Faust} 218*8a52c783SCole Faust``` 219*8a52c783SCole Faust 220*8a52c783SCole FaustThe following requirements must be met for a class annotated with @DynamoDbImmutable: 221*8a52c783SCole Faust1. Every method on the immutable class that is not an override of Object.class or annotated with @DynamoDbIgnore must 222*8a52c783SCole Faust be a getter for an attribute of the database record. 223*8a52c783SCole Faust1. Every getter in the immutable class must have a corresponding setter on the builder class that has a case-sensitive 224*8a52c783SCole Faust matching name. 225*8a52c783SCole Faust1. EITHER: the builder class must have a public default constructor; OR: there must be a public static method named 226*8a52c783SCole Faust 'builder' on the immutable class that takes no parameters and returns an instance of the builder class. 227*8a52c783SCole Faust1. The builder class must have a public method named 'build' that takes no parameters and returns an instance of the 228*8a52c783SCole Faust immutable class. 229*8a52c783SCole Faust 230*8a52c783SCole FaustTo create a TableSchema for your immutable class, use the static constructor method for immutable classes on TableSchema : 231*8a52c783SCole Faust 232*8a52c783SCole Faust```java 233*8a52c783SCole Fauststatic final TableSchema<Customer> CUSTOMER_TABLE_SCHEMA = TableSchema.fromImmutableClass(Customer.class); 234*8a52c783SCole Faust``` 235*8a52c783SCole Faust 236*8a52c783SCole FaustThere are third-party library that help generate a lot of the boilerplate code associated with immutable objects. 237*8a52c783SCole FaustThe DynamoDb Enhanced client should work with these libraries as long as they follow the conventions detailed 238*8a52c783SCole Faustin this section. Here's an example of the immutable Customer class using Lombok with DynamoDb annotations (note 239*8a52c783SCole Fausthow Lombok's 'onMethod' feature is leveraged to copy the attribute based DynamoDb annotations onto the generated code): 240*8a52c783SCole Faust 241*8a52c783SCole Faust```java 242*8a52c783SCole Faust @Value 243*8a52c783SCole Faust @Builder 244*8a52c783SCole Faust @DynamoDbImmutable(builder = Customer.CustomerBuilder.class) 245*8a52c783SCole Faust public static class Customer { 246*8a52c783SCole Faust @Getter(onMethod = @__({@DynamoDbPartitionKey})) 247*8a52c783SCole Faust private String accountId; 248*8a52c783SCole Faust 249*8a52c783SCole Faust @Getter(onMethod = @__({@DynamoDbSortKey})) 250*8a52c783SCole Faust private int subId; 251*8a52c783SCole Faust 252*8a52c783SCole Faust @Getter(onMethod = @__({@DynamoDbSecondaryPartitionKey(indexNames = "customers_by_name")})) 253*8a52c783SCole Faust private String name; 254*8a52c783SCole Faust 255*8a52c783SCole Faust @Getter(onMethod = @__({@DynamoDbSecondarySortKey(indexNames = {"customers_by_date", "customers_by_name"})})) 256*8a52c783SCole Faust private Instant createdDate; 257*8a52c783SCole Faust } 258*8a52c783SCole Faust``` 259*8a52c783SCole Faust 260*8a52c783SCole Faust### Non-blocking asynchronous operations 261*8a52c783SCole FaustIf your application requires non-blocking asynchronous calls to 262*8a52c783SCole FaustDynamoDb, then you can use the asynchronous implementation of the 263*8a52c783SCole Faustmapper. It's very similar to the synchronous implementation with a few 264*8a52c783SCole Faustkey differences: 265*8a52c783SCole Faust 266*8a52c783SCole Faust1. When instantiating the mapped database, use the asynchronous version 267*8a52c783SCole Faust of the library instead of the synchronous one (you will need to use 268*8a52c783SCole Faust an asynchronous DynamoDb client from the SDK as well): 269*8a52c783SCole Faust ```java 270*8a52c783SCole Faust DynamoDbEnhancedAsyncClient enhancedClient = 271*8a52c783SCole Faust DynamoDbEnhancedAsyncClient.builder() 272*8a52c783SCole Faust .dynamoDbClient(dynamoDbAsyncClient) 273*8a52c783SCole Faust .build(); 274*8a52c783SCole Faust ``` 275*8a52c783SCole Faust 276*8a52c783SCole Faust2. Operations that return a single data item will return a 277*8a52c783SCole Faust CompletableFuture of the result instead of just the result. Your 278*8a52c783SCole Faust application can then do other work without having to block on the 279*8a52c783SCole Faust result: 280*8a52c783SCole Faust ```java 281*8a52c783SCole Faust CompletableFuture<Customer> result = mappedTable.getItem(r -> r.key(customerKey)); 282*8a52c783SCole Faust // Perform other work here 283*8a52c783SCole Faust return result.join(); // now block and wait for the result 284*8a52c783SCole Faust ``` 285*8a52c783SCole Faust 286*8a52c783SCole Faust3. Operations that return paginated lists of results will return an 287*8a52c783SCole Faust SdkPublisher of the results instead of an SdkIterable. Your 288*8a52c783SCole Faust application can then subscribe a handler to that publisher and deal 289*8a52c783SCole Faust with the results asynchronously without having to block: 290*8a52c783SCole Faust ```java 291*8a52c783SCole Faust PagePublisher<Customer> results = mappedTable.query(r -> r.queryConditional(keyEqualTo(k -> k.partitionValue("Smith")))); 292*8a52c783SCole Faust results.subscribe(myCustomerResultsProcessor); 293*8a52c783SCole Faust // Perform other work and let the processor handle the results asynchronously 294*8a52c783SCole Faust ``` 295*8a52c783SCole Faust 296*8a52c783SCole Faust## Using extensions 297*8a52c783SCole FaustThe mapper supports plugin extensions to provide enhanced functionality 298*8a52c783SCole Faustbeyond the simple primitive mapped operations. Extensions have two hooks, beforeWrite() and 299*8a52c783SCole FaustafterRead(); the former can modify a write operation before it happens, 300*8a52c783SCole Faustand the latter can modify the results of a read operation after it 301*8a52c783SCole Fausthappens. Some operations such as UpdateItem perform both a write and 302*8a52c783SCole Faustthen a read, so call both hooks. 303*8a52c783SCole Faust 304*8a52c783SCole FaustExtensions are loaded in the order they are specified in the enhanced client builder. This load order can be important, 305*8a52c783SCole Faustas one extension can be acting on values that have been transformed by a previous extension. The client comes with a set 306*8a52c783SCole Faustof pre-written plugin extensions, located in the `/extensions` package. By default (See ExtensionResolver.java) the client loads some of them, 307*8a52c783SCole Faustsuch as VersionedRecordExtension; however, you can override this behavior on the client builder and load any 308*8a52c783SCole Faustextensions you like or specify none if you do not want the ones bundled by default. 309*8a52c783SCole Faust 310*8a52c783SCole FaustIn this example, a custom extension named 'verifyChecksumExtension' is being loaded after the VersionedRecordExtension 311*8a52c783SCole Faustwhich is usually loaded by default by itself: 312*8a52c783SCole Faust```java 313*8a52c783SCole FaustDynamoDbEnhancedClientExtension versionedRecordExtension = VersionedRecordExtension.builder().build(); 314*8a52c783SCole Faust 315*8a52c783SCole FaustDynamoDbEnhancedClient enhancedClient = 316*8a52c783SCole Faust DynamoDbEnhancedClient.builder() 317*8a52c783SCole Faust .dynamoDbClient(dynamoDbClient) 318*8a52c783SCole Faust .extensions(versionedRecordExtension, verifyChecksumExtension) 319*8a52c783SCole Faust .build(); 320*8a52c783SCole Faust``` 321*8a52c783SCole Faust 322*8a52c783SCole Faust### VersionedRecordExtension 323*8a52c783SCole Faust 324*8a52c783SCole FaustThis extension is loaded by default and will increment and track a record version number as 325*8a52c783SCole Faustrecords are written to the database. A condition will be added to every 326*8a52c783SCole Faustwrite that will cause the write to fail if the record version number of 327*8a52c783SCole Faustthe actual persisted record does not match the value that the 328*8a52c783SCole Faustapplication last read. This effectively provides optimistic locking for 329*8a52c783SCole Faustrecord updates, if another process updates a record between the time the 330*8a52c783SCole Faustfirst process has read the record and is writing an update to it then 331*8a52c783SCole Faustthat write will fail. 332*8a52c783SCole Faust 333*8a52c783SCole FaustTo tell the extension which attribute to use to track the record version 334*8a52c783SCole Faustnumber tag a numeric attribute in the TableSchema: 335*8a52c783SCole Faust```java 336*8a52c783SCole Faust @DynamoDbVersionAttribute 337*8a52c783SCole Faust public Integer getVersion() {...}; 338*8a52c783SCole Faust public void setVersion(Integer version) {...}; 339*8a52c783SCole Faust``` 340*8a52c783SCole FaustOr using a StaticTableSchema: 341*8a52c783SCole Faust```java 342*8a52c783SCole Faust .addAttribute(Integer.class, a -> a.name("version") 343*8a52c783SCole Faust .getter(Customer::getVersion) 344*8a52c783SCole Faust .setter(Customer::setVersion) 345*8a52c783SCole Faust // Apply the 'version' tag to the attribute 346*8a52c783SCole Faust .tags(versionAttribute()) 347*8a52c783SCole Faust``` 348*8a52c783SCole Faust 349*8a52c783SCole Faust### AtomicCounterExtension 350*8a52c783SCole Faust 351*8a52c783SCole FaustThis extension is loaded by default and will increment numerical attributes each time records are written to the 352*8a52c783SCole Faustdatabase. Start and increment values can be specified, if not counters start at 0 and increments by 1. 353*8a52c783SCole Faust 354*8a52c783SCole FaustTo tell the extension which attribute is a counter, tag an attribute of type Long in the TableSchema, here using 355*8a52c783SCole Fauststandard values: 356*8a52c783SCole Faust```java 357*8a52c783SCole Faust @DynamoDbAtomicCounter 358*8a52c783SCole Faust public Long getCounter() {...}; 359*8a52c783SCole Faust public void setCounter(Long counter) {...}; 360*8a52c783SCole Faust``` 361*8a52c783SCole FaustOr using a StaticTableSchema with custom values: 362*8a52c783SCole Faust```java 363*8a52c783SCole Faust .addAttribute(Integer.class, a -> a.name("counter") 364*8a52c783SCole Faust .getter(Customer::getCounter) 365*8a52c783SCole Faust .setter(Customer::setCounter) 366*8a52c783SCole Faust // Apply the 'atomicCounter' tag to the attribute with start and increment values 367*8a52c783SCole Faust .tags(atomicCounter(10L, 5L)) 368*8a52c783SCole Faust``` 369*8a52c783SCole Faust 370*8a52c783SCole Faust### AutoGeneratedTimestampRecordExtension 371*8a52c783SCole Faust 372*8a52c783SCole FaustThis extension enables selected attributes to be automatically updated with a current timestamp every time the item 373*8a52c783SCole Faustis successfully written to the database. One requirement is the attribute must be of `Instant` type. 374*8a52c783SCole Faust 375*8a52c783SCole FaustThis extension is not loaded by default, you need to specify it as custom extension while creating the enhanced 376*8a52c783SCole Faustclient. 377*8a52c783SCole Faust 378*8a52c783SCole FaustTo tell the extension which attribute will be updated with the current timestamp, tag the Instant attribute in 379*8a52c783SCole Faustthe TableSchema: 380*8a52c783SCole Faust```java 381*8a52c783SCole Faust @DynamoDbAutoGeneratedTimestampAttribute 382*8a52c783SCole Faust public Instant getLastUpdate() {...} 383*8a52c783SCole Faust public void setLastUpdate(Instant lastUpdate) {...} 384*8a52c783SCole Faust``` 385*8a52c783SCole Faust 386*8a52c783SCole FaustIf using a StaticTableSchema: 387*8a52c783SCole Faust```java 388*8a52c783SCole Faust .addAttribute(Instant.class, a -> a.name("lastUpdate") 389*8a52c783SCole Faust .getter(Customer::getLastUpdate) 390*8a52c783SCole Faust .setter(Customer::setLastUpdate) 391*8a52c783SCole Faust // Applying the 'autoGeneratedTimestamp' tag to the attribute 392*8a52c783SCole Faust .tags(autoGeneratedTimestampAttribute()) 393*8a52c783SCole Faust``` 394*8a52c783SCole Faust 395*8a52c783SCole Faust 396*8a52c783SCole Faust## Advanced table schema features 397*8a52c783SCole Faust### Explicitly include/exclude attributes in DDB mapping 398*8a52c783SCole Faust#### Excluding attributes 399*8a52c783SCole FaustIgnore attributes that should not participate in mapping to DDB 400*8a52c783SCole FaustMark the attribute with the @DynamoDbIgnore annotation: 401*8a52c783SCole Faust```java 402*8a52c783SCole Faustprivate String internalKey; 403*8a52c783SCole Faust 404*8a52c783SCole Faust@DynamoDbIgnore 405*8a52c783SCole Faustpublic String getInternalKey() { return this.internalKey; } 406*8a52c783SCole Faustpublic void setInternalKey(String internalKey) { return this.internalKey = internalKey;} 407*8a52c783SCole Faust``` 408*8a52c783SCole Faust#### Including attributes 409*8a52c783SCole FaustChange the name used to store an attribute in DBB by explicitly marking it with the 410*8a52c783SCole Faust @DynamoDbAttribute annotation and supplying a different name: 411*8a52c783SCole Faust```java 412*8a52c783SCole Faustprivate String internalKey; 413*8a52c783SCole Faust 414*8a52c783SCole Faust@DynamoDbAttribute("renamedInternalKey") 415*8a52c783SCole Faustpublic String getInternalKey() { return this.internalKey; } 416*8a52c783SCole Faustpublic void setInternalKey(String internalKey) { return this.internalKey = internalKey;} 417*8a52c783SCole Faust``` 418*8a52c783SCole Faust 419*8a52c783SCole Faust### Control attribute conversion 420*8a52c783SCole FaustBy default, the table schema provides converters for all primitive and many common Java types 421*8a52c783SCole Faustthrough a default implementation of the AttributeConverterProvider interface. This behavior 422*8a52c783SCole Faustcan be changed both at the attribute converter provider level as well as for a single attribute. 423*8a52c783SCole Faust 424*8a52c783SCole FaustYou can find a list of the available converters in the 425*8a52c783SCole Faust[AttributeConverter](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/AttributeConverter.html) 426*8a52c783SCole Faustinterface Javadoc. 427*8a52c783SCole Faust 428*8a52c783SCole Faust#### Provide custom attribute converter providers 429*8a52c783SCole FaustYou can provide a single AttributeConverterProvider or a chain of ordered AttributeConverterProviders 430*8a52c783SCole Faustthrough the @DynamoDbBean 'converterProviders' annotation. Any custom AttributeConverterProvider must extend the AttributeConverterProvider 431*8a52c783SCole Faustinterface. 432*8a52c783SCole Faust 433*8a52c783SCole FaustNote that if you supply your own chain of attribute converter providers, you will override 434*8a52c783SCole Faustthe default converter provider (DefaultAttributeConverterProvider) and must therefore include it in the chain if you wish to 435*8a52c783SCole Faustuse its attribute converters. It's also possible to annotate the bean with an empty array `{}`, thus 436*8a52c783SCole Faustdisabling the usage of any attribute converter providers including the default, in which case 437*8a52c783SCole Faustall attributes must have their own attribute converters (see below). 438*8a52c783SCole Faust 439*8a52c783SCole FaustSingle converter provider: 440*8a52c783SCole Faust```java 441*8a52c783SCole Faust@DynamoDbBean(converterProviders = ConverterProvider1.class) 442*8a52c783SCole Faustpublic class Customer { 443*8a52c783SCole Faust 444*8a52c783SCole Faust} 445*8a52c783SCole Faust``` 446*8a52c783SCole Faust 447*8a52c783SCole FaustChain of converter providers ending with the default (least priority): 448*8a52c783SCole Faust```java 449*8a52c783SCole Faust@DynamoDbBean(converterProviders = { 450*8a52c783SCole Faust ConverterProvider1.class, 451*8a52c783SCole Faust ConverterProvider2.class, 452*8a52c783SCole Faust DefaultAttributeConverterProvider.class}) 453*8a52c783SCole Faustpublic class Customer { 454*8a52c783SCole Faust 455*8a52c783SCole Faust} 456*8a52c783SCole Faust``` 457*8a52c783SCole Faust 458*8a52c783SCole FaustIn the same way, adding a chain of attribute converter providers directly to a StaticTableSchema: 459*8a52c783SCole Faust```java 460*8a52c783SCole Faustprivate static final StaticTableSchema<Customer> CUSTOMER_TABLE_SCHEMA = 461*8a52c783SCole Faust StaticTableSchema.builder(Customer.class) 462*8a52c783SCole Faust .newItemSupplier(Customer::new) 463*8a52c783SCole Faust .addAttribute(String.class, a -> a.name("name") 464*8a52c783SCole Faust a.getter(Customer::getName) 465*8a52c783SCole Faust a.setter(Customer::setName)) 466*8a52c783SCole Faust .attributeConverterProviders(converterProvider1, converterProvider2) 467*8a52c783SCole Faust .build(); 468*8a52c783SCole Faust``` 469*8a52c783SCole Faust 470*8a52c783SCole Faust#### Override the mapping of a single attribute 471*8a52c783SCole FaustSupply an AttributeConverter when creating the attribute to directly override any 472*8a52c783SCole Faustconverters provided by the table schema AttributeConverterProviders. Note that you will 473*8a52c783SCole Faustonly add a custom converter for that attribute; other attributes, even of the same 474*8a52c783SCole Fausttype, will not use that converter unless explicitly specified for those other attributes. 475*8a52c783SCole Faust 476*8a52c783SCole FaustExample: 477*8a52c783SCole Faust```java 478*8a52c783SCole Faust@DynamoDbBean 479*8a52c783SCole Faustpublic class Customer { 480*8a52c783SCole Faust private String name; 481*8a52c783SCole Faust 482*8a52c783SCole Faust @DynamoDbConvertedBy(CustomAttributeConverter.class) 483*8a52c783SCole Faust public String getName() { return this.name; } 484*8a52c783SCole Faust public void setName(String name) { this.name = name;} 485*8a52c783SCole Faust} 486*8a52c783SCole Faust``` 487*8a52c783SCole FaustFor StaticTableSchema: 488*8a52c783SCole Faust```java 489*8a52c783SCole Faustprivate static final StaticTableSchema<Customer> CUSTOMER_TABLE_SCHEMA = 490*8a52c783SCole Faust StaticTableSchema.builder(Customer.class) 491*8a52c783SCole Faust .newItemSupplier(Customer::new) 492*8a52c783SCole Faust .addAttribute(String.class, a -> a.name("name") 493*8a52c783SCole Faust a.getter(Customer::getName) 494*8a52c783SCole Faust a.setter(Customer::setName) 495*8a52c783SCole Faust a.attributeConverter(customAttributeConverter)) 496*8a52c783SCole Faust .build(); 497*8a52c783SCole Faust``` 498*8a52c783SCole Faust 499*8a52c783SCole Faust### Changing update behavior of attributes 500*8a52c783SCole FaustIt is possible to customize the update behavior as applicable to individual attributes when an 'update' operation is 501*8a52c783SCole Faustperformed (e.g. UpdateItem or an update within TransactWriteItems). 502*8a52c783SCole Faust 503*8a52c783SCole FaustFor example, say like you wanted to store a 'created on' timestamp on your record, but only wanted its value to be 504*8a52c783SCole Faustwritten if there is no existing value for the attribute stored in the database then you would use the 505*8a52c783SCole FaustWRITE_IF_NOT_EXISTS update behavior. Here is an example using a bean: 506*8a52c783SCole Faust 507*8a52c783SCole Faust```java 508*8a52c783SCole Faust@DynamoDbBean 509*8a52c783SCole Faustpublic class Customer extends GenericRecord { 510*8a52c783SCole Faust private String id; 511*8a52c783SCole Faust private Instant createdOn; 512*8a52c783SCole Faust 513*8a52c783SCole Faust @DynamoDbPartitionKey 514*8a52c783SCole Faust public String getId() { return this.id; } 515*8a52c783SCole Faust public void setId(String id) { this.name = id; } 516*8a52c783SCole Faust 517*8a52c783SCole Faust @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) 518*8a52c783SCole Faust public Instant getCreatedOn() { return this.createdOn; } 519*8a52c783SCole Faust public void setCreatedOn(Instant createdOn) { this.createdOn = createdOn; } 520*8a52c783SCole Faust} 521*8a52c783SCole Faust``` 522*8a52c783SCole Faust 523*8a52c783SCole FaustSame example using a static table schema: 524*8a52c783SCole Faust 525*8a52c783SCole Faust```java 526*8a52c783SCole Fauststatic final TableSchema<Customer> CUSTOMER_TABLE_SCHEMA = 527*8a52c783SCole Faust TableSchema.builder(Customer.class) 528*8a52c783SCole Faust .newItemSupplier(Customer::new) 529*8a52c783SCole Faust .addAttribute(String.class, a -> a.name("id") 530*8a52c783SCole Faust .getter(Customer::getId) 531*8a52c783SCole Faust .setter(Customer::setId) 532*8a52c783SCole Faust .tags(primaryPartitionKey())) 533*8a52c783SCole Faust .addAttribute(Instant.class, a -> a.name("createdOn") 534*8a52c783SCole Faust .getter(Customer::getCreatedOn) 535*8a52c783SCole Faust .setter(Customer::setCreatedOn) 536*8a52c783SCole Faust .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) 537*8a52c783SCole Faust .build(); 538*8a52c783SCole Faust``` 539*8a52c783SCole Faust 540*8a52c783SCole Faust### Flat map attributes from another class 541*8a52c783SCole FaustIf the attributes for your table record are spread across several 542*8a52c783SCole Faustdifferent Java objects, either through inheritance or composition, the 543*8a52c783SCole Fauststatic TableSchema implementation gives you a method of flat mapping 544*8a52c783SCole Faustthose attributes and rolling them up into a single schema. 545*8a52c783SCole Faust 546*8a52c783SCole Faust#### Using inheritance 547*8a52c783SCole FaustTo accomplish flat map using inheritance, the only requirement is that 548*8a52c783SCole Faustboth classes are annotated as a DynamoDb bean: 549*8a52c783SCole Faust 550*8a52c783SCole Faust```java 551*8a52c783SCole Faust@DynamoDbBean 552*8a52c783SCole Faustpublic class Customer extends GenericRecord { 553*8a52c783SCole Faust private String name; 554*8a52c783SCole Faust private GenericRecord record; 555*8a52c783SCole Faust 556*8a52c783SCole Faust public String getName() { return this.name; } 557*8a52c783SCole Faust public void setName(String name) { this.name = name;} 558*8a52c783SCole Faust 559*8a52c783SCole Faust public GenericRecord getRecord() { return this.record; } 560*8a52c783SCole Faust public void setRecord(GenericRecord record) { this.record = record;} 561*8a52c783SCole Faust} 562*8a52c783SCole Faust 563*8a52c783SCole Faust@DynamoDbBean 564*8a52c783SCole Faustpublic abstract class GenericRecord { 565*8a52c783SCole Faust private String id; 566*8a52c783SCole Faust private String createdDate; 567*8a52c783SCole Faust 568*8a52c783SCole Faust public String getId() { return this.id; } 569*8a52c783SCole Faust public void setId(String id) { this.id = id;} 570*8a52c783SCole Faust 571*8a52c783SCole Faust public String getCreatedDate() { return this.createdDate; } 572*8a52c783SCole Faust public void setCreatedDate(String createdDate) { this.createdDate = createdDate;} 573*8a52c783SCole Faust} 574*8a52c783SCole Faust 575*8a52c783SCole Faust``` 576*8a52c783SCole Faust 577*8a52c783SCole FaustFor StaticTableSchema, use the 'extend' feature to achieve the same effect: 578*8a52c783SCole Faust```java 579*8a52c783SCole Faust@Data 580*8a52c783SCole Faustpublic class Customer extends GenericRecord { 581*8a52c783SCole Faust private String name; 582*8a52c783SCole Faust} 583*8a52c783SCole Faust 584*8a52c783SCole Faust@Data 585*8a52c783SCole Faustpublic abstract class GenericRecord { 586*8a52c783SCole Faust private String id; 587*8a52c783SCole Faust private String createdDate; 588*8a52c783SCole Faust} 589*8a52c783SCole Faust 590*8a52c783SCole Faustprivate static final StaticTableSchema<GenericRecord> GENERIC_RECORD_SCHEMA = 591*8a52c783SCole Faust StaticTableSchema.builder(GenericRecord.class) 592*8a52c783SCole Faust // The partition key will be inherited by the top level mapper 593*8a52c783SCole Faust .addAttribute(String.class, a -> a.name("id") 594*8a52c783SCole Faust .getter(GenericRecord::getId) 595*8a52c783SCole Faust .setter(GenericRecord::setId) 596*8a52c783SCole Faust .tags(primaryPartitionKey())) 597*8a52c783SCole Faust .addAttribute(String.class, a -> a.name("created_date") 598*8a52c783SCole Faust .getter(GenericRecord::getCreatedDate) 599*8a52c783SCole Faust .setter(GenericRecord::setCreatedDate)) 600*8a52c783SCole Faust .build(); 601*8a52c783SCole Faust 602*8a52c783SCole Faustprivate static final StaticTableSchema<Customer> CUSTOMER_TABLE_SCHEMA = 603*8a52c783SCole Faust StaticTableSchema.builder(Customer.class) 604*8a52c783SCole Faust .newItemSupplier(Customer::new) 605*8a52c783SCole Faust .addAttribute(String.class, a -> a.name("name") 606*8a52c783SCole Faust .getter(Customer::getName) 607*8a52c783SCole Faust .setter(Customer::setName)) 608*8a52c783SCole Faust .extend(GENERIC_RECORD_SCHEMA) // All the attributes of the GenericRecord schema are added to Customer 609*8a52c783SCole Faust .build(); 610*8a52c783SCole Faust``` 611*8a52c783SCole Faust#### Using composition 612*8a52c783SCole Faust 613*8a52c783SCole FaustUsing composition, the @DynamoDbFlatten annotation flat maps the composite class: 614*8a52c783SCole Faust```java 615*8a52c783SCole Faust@DynamoDbBean 616*8a52c783SCole Faustpublic class Customer { 617*8a52c783SCole Faust private String name; 618*8a52c783SCole Faust private GenericRecord record; 619*8a52c783SCole Faust 620*8a52c783SCole Faust public String getName() { return this.name; } 621*8a52c783SCole Faust public void setName(String name) { this.name = name;} 622*8a52c783SCole Faust 623*8a52c783SCole Faust @DynamoDbFlatten 624*8a52c783SCole Faust public GenericRecord getRecord() { return this.record; } 625*8a52c783SCole Faust public void setRecord(GenericRecord record) { this.record = record;} 626*8a52c783SCole Faust} 627*8a52c783SCole Faust 628*8a52c783SCole Faust@DynamoDbBean 629*8a52c783SCole Faustpublic class GenericRecord { 630*8a52c783SCole Faust private String id; 631*8a52c783SCole Faust private String createdDate; 632*8a52c783SCole Faust 633*8a52c783SCole Faust public String getId() { return this.id; } 634*8a52c783SCole Faust public void setId(String id) { this.id = id;} 635*8a52c783SCole Faust 636*8a52c783SCole Faust public String getCreatedDate() { return this.createdDate; } 637*8a52c783SCole Faust public void setCreatedDate(String createdDate) { this.createdDate = createdDate;} 638*8a52c783SCole Faust} 639*8a52c783SCole Faust``` 640*8a52c783SCole FaustYou can flatten as many different eligible classes as you like using the flatten annotation. 641*8a52c783SCole FaustThe only constraints are that attributes must not have the same name when they are being rolled 642*8a52c783SCole Fausttogether, and there must never be more than one partition key, sort key or table name. 643*8a52c783SCole Faust 644*8a52c783SCole FaustFlat map composite classes using StaticTableSchema: 645*8a52c783SCole Faust 646*8a52c783SCole Faust```java 647*8a52c783SCole Faust@Data 648*8a52c783SCole Faustpublic class Customer{ 649*8a52c783SCole Faust private String name; 650*8a52c783SCole Faust private GenericRecord recordMetadata; 651*8a52c783SCole Faust //getters and setters for all attributes 652*8a52c783SCole Faust} 653*8a52c783SCole Faust 654*8a52c783SCole Faust@Data 655*8a52c783SCole Faustpublic class GenericRecord { 656*8a52c783SCole Faust private String id; 657*8a52c783SCole Faust private String createdDate; 658*8a52c783SCole Faust //getters and setters for all attributes 659*8a52c783SCole Faust} 660*8a52c783SCole Faust 661*8a52c783SCole Faustprivate static final StaticTableSchema<GenericRecord> GENERIC_RECORD_SCHEMA = 662*8a52c783SCole Faust StaticTableSchema.builder(GenericRecord.class) 663*8a52c783SCole Faust .addAttribute(String.class, a -> a.name("id") 664*8a52c783SCole Faust .getter(GenericRecord::getId) 665*8a52c783SCole Faust .setter(GenericRecord::setId) 666*8a52c783SCole Faust .tags(primaryPartitionKey())) 667*8a52c783SCole Faust .addAttribute(String.class, a -> a.name("created_date") 668*8a52c783SCole Faust .getter(GenericRecord::getCreatedDate) 669*8a52c783SCole Faust .setter(GenericRecord::setCreatedDate)) 670*8a52c783SCole Faust .build(); 671*8a52c783SCole Faust 672*8a52c783SCole Faustprivate static final StaticTableSchema<Customer> CUSTOMER_TABLE_SCHEMA = 673*8a52c783SCole Faust StaticTableSchema.builder(Customer.class) 674*8a52c783SCole Faust .newItemSupplier(Customer::new) 675*8a52c783SCole Faust .addAttribute(String.class, a -> a.name("name") 676*8a52c783SCole Faust .getter(Customer::getName) 677*8a52c783SCole Faust .setter(Customer::setName)) 678*8a52c783SCole Faust // Because we are flattening a component object, we supply a getter and setter so the 679*8a52c783SCole Faust // mapper knows how to access it 680*8a52c783SCole Faust .flatten(GENERIC_RECORD_SCHEMA, Customer::getRecordMetadata, Customer::setRecordMetadata) 681*8a52c783SCole Faust .build(); 682*8a52c783SCole Faust``` 683*8a52c783SCole FaustJust as for annotations, you can flatten as many different eligible classes as you like using the 684*8a52c783SCole Faustbuilder pattern. 685