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