xref: /aosp_15_r20/external/aws-sdk-java-v2/services-custom/dynamodb-enhanced/README.md (revision 8a52c7834d808308836a99fc2a6e0ed8db339086)
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