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