

# Use advanced mapping features
<a name="ddb-en-client-adv-features"></a>

Learn about advanced table schema features in the DynamoDB Enhanced Client API.

## Understand table schema types
<a name="ddb-en-client-adv-features-schm-overview"></a>

`[TableSchema](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/TableSchema.html)` is the interface to the mapping functionality of the DynamoDB Enhanced Client API. It can map a data object to and from a map of [AttributeValues](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/services/dynamodb/model/AttributeValue.html). A `TableSchema` object needs to know about the structure of the table it is mapping. This structure information is stored in a [https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/TableMetadata.html](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/TableMetadata.html) object.

The enhanced client API has several implementations of `TableSchema`, which follow. 

### Table schema generated from annotated classes
<a name="ddb-en-client-adv-features-schema-mapped"></a>

It is a moderately expensive operation to build a `TableSchema` from annotated classes, so we recommend doing this once, at application startup.

 [ BeanTableSchema ](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/mapper/BeanTableSchema.html)   
This implementation is built based on attributes and annotations of a bean class. An example of this approach is demonstrated in the [Get started section](ddb-en-client-gs-tableschema.md#ddb-en-client-gs-tableschema-anno-bean).  
If a `BeanTableSchema` is not behaving as you expect, enable debug logging for `software.amazon.awssdk.enhanced.dynamodb.beans`.

[ImmutableTableSchema](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableTableSchema.html)  
This implementation is built from an immutable data class. This approach is described in the [Work with immutable data classes](ddb-en-client-use-immut.md) section.

### Table schema generated with a builder
<a name="ddb-en-client-adv-features-schema-static"></a>

The following `TableSchema`s are built from code by using a builder. This approach is less costly than the approach that uses annotated data classes. The builder approach avoids the use of annotations and doesn't require JavaBean naming standards.

[StaticTableSchema](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticTableSchema.html)  
This implementation is built for mutable data classes. The getting started section of this guide demonstrated how to [generate a `StaticTableSchema` using a builder](ddb-en-client-gs-tableschema.md#ddb-en-client-gs-tableschema-builder).

[StaticImmutableTableSchema](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticImmutableTableSchema.html)  
Similarly to how you build a `StaticTableSchema`, you generate an implementation of this type of `TableSchema` using a [builder](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticImmutableTableSchema.html) for use with immutable data classes.

### Table schema for data without a fixed schema
<a name="ddb-en-client-adv-features-schema-document"></a>

[DocumentTableSchema](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/document/DocumentTableSchema.html)  
Unlike other implementations of `TableSchema`, you don't define attributes for a `DocumentTableSchema` instance. Usually, you specify only primary keys and attribute converter providers. An `EnhancedDocument` instance provides the attributes that you build from individual elements or from a JSON string.

# Explicitly include or exclude attributes
<a name="ddb-en-client-adv-features-inex-attr"></a>

The DynamoDB Enhanced Client API offers annotations to exclude data class attributes from becoming attributes on a table. With the API, you can also use an attribute name that's different from the data class attribute name.

## Exclude attributes
<a name="ddb-en-client-adv-features-inex-attr-ex"></a>

To ignore attributes that should not be mapped to a DynamoDB table, mark the attribute with the `@DynamoDbIgnore` annotation.

```
private String internalKey;

@DynamoDbIgnore
public String getInternalKey() { return this.internalKey; }
public void setInternalKey(String internalKey) { this.internalKey = internalKey;}
```

## Include attributes
<a name="ddb-en-client-adv-features-inex-attr-in"></a>

To change the name of an attribute used in the DynamoDB table, mark it with the `@DynamoDbAttribute` annotation and supply a different name.

```
private String internalKey;

@DynamoDbAttribute("renamedInternalKey")
public String getInternalKey() { return this.internalKey; }
public void setInternalKey(String internalKey) { this.internalKey = internalKey;}
```

# Control attribute conversion
<a name="ddb-en-client-adv-features-conversion"></a>

By default, a table schema provides converters for many common Java types through a default implementation of the `[AttributeConverterProvider](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/AttributeConverterProvider.html)` interface. You can change the overall default behavior with a custom `AttributeConverterProvider` implementation. You can also change the converter for a single attribute.

For a list of available converters, see the [AttributeConverter](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/AttributeConverter.html) interface Java doc.

## Provide custom attribute converter providers
<a name="ddb-en-client-adv-features-conversion-prov"></a>

You can provide a single `AttributeConverterProvider` or a chain of ordered `AttributeConverterProvider`s through the `@DynamoDbBean` `(converterProviders = {…})` annotation. Any custom `AttributeConverterProvider` must extend the `AttributeConverterProvider` interface.

Note that if you supply your own chain of attribute converter providers, you will override the default converter provider, `DefaultAttributeConverterProvider`. If you want to use the functionality of the `DefaultAttributeConverterProvider`, you must include it in the chain. 

It's also possible to annotate the bean with an empty array `{}`. This disables the use of any attribute converter providers, including the default. In this case all attributes that are to be mapped must have their own attribute converter.

The following snippet shows a single converter provider.

```
@DynamoDbBean(converterProviders = ConverterProvider1.class)
public class Customer {

}
```

The following snippet shows the use of a chain of converter providers. Since the SDK default is provided last, it has the lowest priority.

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

}
```

The static table schema builders have an `attributeConverterProviders()` method that works the same way. This is shown in the following snippet.

```
private static final StaticTableSchema<Customer> CUSTOMER_TABLE_SCHEMA =
  StaticTableSchema.builder(Customer.class)
    .newItemSupplier(Customer::new)
    .addAttribute(String.class, a -> a.name("name")
                                     a.getter(Customer::getName)
                                     a.setter(Customer::setName))
    .attributeConverterProviders(converterProvider1, converterProvider2)
    .build();
```

## Override the mapping of a single attribute
<a name="ddb-en-client-adv-features-conversion-single"></a>

To override the way a single attribute is mapped, supply an `AttributeConverter` for the attribute. This addition overrides any converters provided by `AttributeConverterProviders` in the table schema. This adds a custom converter for only that attribute. Other attributes, even those of the same type, won't use that converter unless it is explicitly specified for those other attributes.

The `@DynamoDbConvertedBy` annotation is used to specify the custom `AttributeConverter` class as shown in the following snippet.

```
@DynamoDbBean
public class Customer {
    private String name;

    @DynamoDbConvertedBy(CustomAttributeConverter.class)
    public String getName() { return this.name; }
    public void setName(String name) { this.name = name;}
}
```

The builders for static schemas have an equivalent attribute builder `attributeConverter()` method. This method takes an instance of an `AttributeConverter` as the following shows.

```
private static final StaticTableSchema<Customer> CUSTOMER_TABLE_SCHEMA =
  StaticTableSchema.builder(Customer.class)
    .newItemSupplier(Customer::new)
    .addAttribute(String.class, a -> a.name("name")
                                     a.getter(Customer::getName)
                                     a.setter(Customer::setName)
                                     a.attributeConverter(customAttributeConverter))
    .build();
```

## Example
<a name="ddb-en-client-adv-features-conversion-example"></a>

This example shows an `AttributeConverterProvider` implementation that provides an attribute converter for [https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/net/HttpCookie.html](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/net/HttpCookie.html) objects. 

The following `SimpleUser` class contains an attribute named `lastUsedCookie` that is an instance of `HttpCookie`.

The parameter to the `@DynamoDbBean` annotations lists the two `AttributeConverterProvider` classes that provide converters.

------
#### [ Class with annotations ]

```
    @DynamoDbBean(converterProviders = {CookieConverterProvider.class, DefaultAttributeConverterProvider.class})
    public static final class SimpleUser {
        private String name;
        private HttpCookie lastUsedCookie;

        @DynamoDbPartitionKey
        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public HttpCookie getLastUsedCookie() {
            return lastUsedCookie;
        }

        public void setLastUsedCookie(HttpCookie lastUsedCookie) {
            this.lastUsedCookie = lastUsedCookie;
        }
```

------
#### [ Static table schema ]

```
    private static final TableSchema<SimpleUser> SIMPLE_USER_TABLE_SCHEMA =
            TableSchema.builder(SimpleUser.class)
                    .newItemSupplier(SimpleUser::new)
                    .attributeConverterProviders(CookieConverterProvider.create(), AttributeConverterProvider.defaultProvider())
                    .addAttribute(String.class, a -> a.name("name")
                            .setter(SimpleUser::setName)
                            .getter(SimpleUser::getName)
                            .tags(StaticAttributeTags.primaryPartitionKey()))
                    .addAttribute(HttpCookie.class, a -> a.name("lastUsedCookie")
                            .setter(SimpleUser::setLastUsedCookie)
                            .getter(SimpleUser::getLastUsedCookie))
                    .build();
```

------

The `CookieConverterProvider` in the following example provides an instance of an `HttpCookeConverter`.

```
    public static final class CookieConverterProvider implements AttributeConverterProvider {
        private final Map<EnhancedType<?>, AttributeConverter<?>> converterCache = ImmutableMap.of(
                // 1. Add HttpCookieConverter to the internal cache.
                EnhancedType.of(HttpCookie.class), new HttpCookieConverter());

        public static CookieConverterProvider create() {
            return new CookieConverterProvider();
        }

        // The SDK calls this method to find out if the provider contains a AttributeConverter instance
        // for the EnhancedType<T> argument.
        @SuppressWarnings("unchecked")
        @Override
        public <T> AttributeConverter<T> converterFor(EnhancedType<T> enhancedType) {
            return (AttributeConverter<T>) converterCache.get(enhancedType);
        }
    }
```

### Conversion code
<a name="ddb-en-client-adv-features-conversion-example-code"></a>

In the `transformFrom()` method of the following `HttpCookieConverter` class, the code receives an `HttpCookie` instance and transforms it into a DynamoDB map that is stored as an attribute.

The `transformTo()` method receives a DynamoDB map parameter, then invokes the `HttpCookie` constructor that requires a name and a value.

```
    public static final class HttpCookieConverter implements AttributeConverter<HttpCookie> {

        @Override
        public AttributeValue transformFrom(HttpCookie httpCookie) {

            return AttributeValue.fromM(
            Map.of ("cookieName", AttributeValue.fromS(httpCookie.getName()),
                    "cookieValue", AttributeValue.fromS(httpCookie.getValue()))
            );
        }

        @Override
        public HttpCookie transformTo(AttributeValue attributeValue) {
            Map<String, AttributeValue> map = attributeValue.m();
            return new HttpCookie(
                    map.get("cookieName").s(),
                    map.get("cookieValue").s());
        }

        @Override
        public EnhancedType<HttpCookie> type() {
            return EnhancedType.of(HttpCookie.class);
        }

        @Override
        public AttributeValueType attributeValueType() {
            return AttributeValueType.M;
        }
    }
```

# Change update behavior of attributes
<a name="ddb-en-client-adv-features-upd-behavior"></a>

You can customize the update behavior of individual attributes when you perform an *update* operation. Some examples of update operations in the DynamoDB Enhanced Client API are [updateItem()](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/DynamoDbTable.html#updateItem(T)) and [transactWriteItems()](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/DynamoDbEnhancedClient.html#transactWriteItems(java.util.function.Consumer)).

For example, imagine that you want to store a *created on* timestamp on your record. However, you want its value to be written only if there's no existing value for the attribute already in the database. In this case, you use the `[WRITE\$1IF\$1NOT\$1EXISTS](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/mapper/UpdateBehavior.html#WRITE_IF_NOT_EXISTS)` update behavior.

The following example shows the annotation that adds the behavior to the `createdOn` attribute.

```
@DynamoDbBean
public class Customer extends GenericRecord {
    private String id;
    private Instant createdOn;

    @DynamoDbPartitionKey
    public String getId() { return this.id; }
    public void setId(String id) { this.name = id; }

    @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS)
    public Instant getCreatedOn() { return this.createdOn; }    
    public void setCreatedOn(Instant createdOn) { this.createdOn = createdOn; }
}
```

You can declare the same update behavior when you build a static table schema as shown in the following example after comment line 1.

```
static final TableSchema<Customer> CUSTOMER_TABLE_SCHEMA =
     TableSchema.builder(Customer.class)
       .newItemSupplier(Customer::new)
       .addAttribute(String.class, a -> a.name("id")
                                         .getter(Customer::getId)
                                         .setter(Customer::setId)
                                         .tags(StaticAttributeTags.primaryPartitionKey()))
       .addAttribute(Instant.class, a -> a.name("createdOn")
                                          .getter(Customer::getCreatedOn)
                                          .setter(Customer::setCreatedOn)
                                          // 1. Add an UpdateBehavior.
                                          .tags(StaticAttributeTags.updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS)))
       .build();
```

# Flatten attributes from other classes
<a name="ddb-en-client-adv-features-flatmap"></a>

If the attributes for your table are spread across several different Java classes, either through inheritance or composition, the DynamoDB Enhanced Client API provides support to flatten the attributes into one class.

## Use inheritance
<a name="ddb-en-client-adv-features-flatmap-inheritance"></a>

If your classes use inheritance, use the following approaches to flatten the hierarchy.

### Use annotated beans
<a name="ddb-en-client-adv-features-flatmap-inheritance-anno"></a>

For the annotation approach, both classes must carry the `@DynamoDbBean` annotation and a class must carry one or more primary key annotations.

The following shows examples of data classes that have an inheritance relationship.

------
#### [ Standard data class ]

```
@DynamoDbBean
public class Customer extends GenericRecord {
    private String name;

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
}

@DynamoDbBean
public abstract class GenericRecord {
    private String id;
    private String createdDate;

    @DynamoDbPartitionKey
    public String getId() { return id; }
    public void setId(String id) { this.id = id; }

    public String getCreatedDate() { return createdDate; }
    public void setCreatedDate(String createdDate) { this.createdDate = createdDate; }
}
```

------
#### [ Lombok ]

Lombok's [`onMethod` option](https://projectlombok.org/features/experimental/onX) copies attribute-based DynamoDB annotations, such as `@DynamoDbPartitionKey`, onto the generated code.

```
@DynamoDbBean
@Data
@ToString(callSuper = true)
public class Customer extends GenericRecord {
    private String name;
}

@Data
@DynamoDbBean
public abstract class GenericRecord {
    @Getter(onMethod_=@DynamoDbPartitionKey)
    private String id;
    private String createdDate;
}
```

------

### Use static schemas
<a name="ddb-en-client-adv-features-flatmap-inheritance-static"></a>

For the static schema approach, use the `extend()` method of the builder to collapse the attributes of the parent class onto the child class. This is shown after comment line 1 in the following example.

```
        StaticTableSchema<org.example.tests.model.inheritance.stat.GenericRecord> GENERIC_RECORD_SCHEMA =
                StaticTableSchema.builder(org.example.tests.model.inheritance.stat.GenericRecord.class)
                        // The partition key will be inherited by the top level mapper.
                        .addAttribute(String.class, a -> a.name("id")
                                .getter(org.example.tests.model.inheritance.stat.GenericRecord::getId)
                                .setter(org.example.tests.model.inheritance.stat.GenericRecord::setId)
                                .tags(primaryPartitionKey()))
                        .addAttribute(String.class, a -> a.name("created_date")
                                .getter(org.example.tests.model.inheritance.stat.GenericRecord::getCreatedDate)
                                .setter(org.example.tests.model.inheritance.stat.GenericRecord::setCreatedDate))
                        .build();

        StaticTableSchema<org.example.tests.model.inheritance.stat.Customer> CUSTOMER_SCHEMA =
                StaticTableSchema.builder(org.example.tests.model.inheritance.stat.Customer.class)
                        .newItemSupplier(org.example.tests.model.inheritance.stat.Customer::new)
                        .addAttribute(String.class, a -> a.name("name")
                                .getter(org.example.tests.model.inheritance.stat.Customer::getName)
                                .setter(org.example.tests.model.inheritance.stat.Customer::setName))
                        // 1. Use the extend() method to collapse the parent attributes onto the child class.
                        .extend(GENERIC_RECORD_SCHEMA)     // All the attributes of the GenericRecord schema are added to Customer.
                        .build();
```

The previous static schema example uses the following data classes. Because the mapping is defined when you build the static table schema, the data classes don't require annotations.

#### Data classes
<a name="gunk"></a>

------
#### [ Standard data class ]

```
public class Customer extends GenericRecord {
    private String name;

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
}


public abstract class GenericRecord {
    private String id;
    private String createdDate;

    public String getId() { return id; }
    public void setId(String id) { this.id = id; }

    public String getCreatedDate() { return createdDate; }
    public void setCreatedDate(String createdDate) { this.createdDate = createdDate; }
```

------
#### [ Lombok ]

```
@Data
@ToString(callSuper = true)
public class Customer extends GenericRecord{
    private String name;
}

@Data
public abstract class GenericRecord {
    private String id;
    private String createdDate;
}
```

------

## Use composition
<a name="ddb-en-client-adv-features-flatmap-comp"></a>

If your classes use composition, use the following approaches to flatten the hierarchy.

### Use annotated beans
<a name="ddb-en-client-adv-features-flatmap-comp-anno"></a>

The `@DynamoDbFlatten` annotation flattens the contained class.

The following data class examples use the `@DynamoDbFlatten` annotation to effectively add all attributes of the contained `GenericRecord` class to the `Customer` class.

------
#### [ Standard data class ]

```
@DynamoDbBean
public class Customer {
    private String name;
    private GenericRecord record;

    public String getName() { return this.name; }
    public void setName(String name) { this.name = name; }

    @DynamoDbFlatten
    public GenericRecord getRecord() { return this.record; }
    public void setRecord(GenericRecord record) { this.record = record; }

@DynamoDbBean
public class GenericRecord {
    private String id;
    private String createdDate;

    @DynamoDbPartitionKey
    public String getId() { return this.id; }
    public void setId(String id) { this.id = id; }

    public String getCreatedDate() { return this.createdDate; }
    public void setCreatedDate(String createdDate) { this.createdDate = createdDate; }
}
```

------
#### [ Lombok ]

```
@Data
@DynamoDbBean
public class Customer {
    private String name;
    @Getter(onMethod_=@DynamoDbFlatten)
    private GenericRecord record;
}

@Data
@DynamoDbBean
public class GenericRecord {
    @Getter(onMethod_=@DynamoDbPartitionKey)
    private String id;
    private String createdDate;
}
```

------

You can use the flatten annotation to flatten as many different eligible classes as you need to. The following constraints apply:
+ All attribute names must be unique after they are flattened.
+ There must never be more than one partition key, sort key, or table name.

### Use static schemas
<a name="ddb-en-client-adv-features-flatmap-comp-static"></a>

When you build a static table schema, use the `flatten()` method of the builder. You also supply the getter and setter methods that identify the contained class.

```
        StaticTableSchema<GenericRecord> GENERIC_RECORD_SCHEMA =
                StaticTableSchema.builder(GenericRecord.class)
                        .newItemSupplier(GenericRecord::new)
                        .addAttribute(String.class, a -> a.name("id")
                                .getter(GenericRecord::getId)
                                .setter(GenericRecord::setId)
                                .tags(primaryPartitionKey()))
                        .addAttribute(String.class, a -> a.name("created_date")
                                .getter(GenericRecord::getCreatedDate)
                                .setter(GenericRecord::setCreatedDate))
                        .build();

        StaticTableSchema<Customer> CUSTOMER_SCHEMA =
                StaticTableSchema.builder(Customer.class)
                        .newItemSupplier(Customer::new)
                        .addAttribute(String.class, a -> a.name("name")
                                .getter(Customer::getName)
                                .setter(Customer::setName))
                        // Because we are flattening a component object, we supply a getter and setter so the
                        // mapper knows how to access it.
                        .flatten(GENERIC_RECORD_SCHEMA, Customer::getRecord, Customer::setRecord)
                        .build();
```

The previous static schema example uses the following data classes.

#### Data classes
<a name="ddb-en-client-adv-features-flatmap-comp-static-supporting"></a>

------
#### [ Standard data class ]

```
public class Customer {
    private String name;
    private GenericRecord record;

    public String getName() { return this.name; }
    public void setName(String name) { this.name = name; }

    public GenericRecord getRecord() { return this.record; }
    public void setRecord(GenericRecord record) { this.record = record; }

public class GenericRecord {
    private String id;
    private String createdDate;

    public String getId() { return this.id; }
    public void setId(String id) { this.id = id; }

    public String getCreatedDate() { return this.createdDate; }
    public void setCreatedDate(String createdDate) { this.createdDate = createdDate; }
}
```

------
#### [ Lombok ]

```
@Data
public class Customer {
    private String name;
    private GenericRecord record;
}

@Data
public class GenericRecord {
    private String id;
    private String createdDate;
}
```

------

You can use the builder pattern to flatten as many different eligible classes as you need to.

## Implications for other code
<a name="ddb-en-client-adv-features-flatmap-compare"></a>

When you use the `@DynamoDbFlatten` attribute (or `flatten()` builder method), the item in DynamoDB contains an attribute for each attribute of the composed object. It also includes the attributes of the composing object. 

In contrast, if you annotate a data class with a composed class and don't use `@DynamoDbFlatten`, the item is saved with the composed object as a single attribute.

For example, compare the `Customer` class shown in the [flattening with composition example](#ddb-en-client-adv-features-flatmap-comp-anno) with and without flattening of the `record` attribute. You can visualize the difference with JSON as shown in the following table.


****  

| With flattening | Without flattening | 
| --- | --- | 
| 3 attributes | 2 attributes | 
|  <pre>{<br />  "id": "1",<br />  "createdDate": "today",<br />  "name": "my name"<br />}</pre>  |  <pre>{<br />  "id": "1",<br />  "record": {<br />      "createdDate": "today",<br />      "name": "my name"<br />  }<br />}</pre>  | 

The difference becomes important if you have other code accessing the DynamoDB table that expects to find certain attributes.

# Work with attributes that are beans, maps, lists and sets
<a name="ddb-en-client-adv-features-nested"></a>

A bean definition, such as the `Person` class shown below, might define properties (or attributes) that refer to types with additional attributes. For instance, in the `Person` class, `mainAddress` is a property that refers to an `Address` bean that defines additional value attributes. `addresses` refers to a Java Map, whose elements refer to `Address` beans. These complex types can be thought of a containers of simple attributes that you use for their data value in the context of DynamoDB. 

DynamoDB refers to the value properties of nested elements, such as maps, lists, or beans, as *nested attributes*. The [Amazon DynamoDB Developer Guide](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes) refers to the saved form of a Java map, list or bean as a *document type*. Simple attributes that you use for their data value in Java are referred to as *scalar types* in DynamoDB. Sets, which contains multiple scalar elements of the same type, and referred to as *set types*. 

It is important to know that the DynamoDB Enhanced Client API converts a property that is bean to a DynamoDB map document type when it is save.

## `Person` class
<a name="ddb-en-client-adv-features-nested-person"></a>

```
@DynamoDbBean
public class Person {
    private Integer id;
    private String firstName;
    private String lastName;
    private Integer age;
    private Address mainAddress;
    private Map<String, Address> addresses;
    private List<PhoneNumber> phoneNumbers;
    private Set<String> hobbies;

    @DynamoDbPartitionKey
    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public Address getMainAddress() {
        return mainAddress;
    }

    public void setMainAddress(Address mainAddress) {
        this.mainAddress = mainAddress;
    }

    public Map<String, Address> getAddresses() {
        return addresses;
    }

    public void setAddresses(Map<String, Address> addresses) {
        this.addresses = addresses;
    }

    public List<PhoneNumber> getPhoneNumbers() {
        return phoneNumbers;
    }

    public void setPhoneNumbers(List<PhoneNumber> phoneNumbers) {
        this.phoneNumbers = phoneNumbers;
    }

    public Set<String> getHobbies() {
        return hobbies;
    }

    public void setHobbies(Set<String> hobbies) {
        this.hobbies = hobbies;
    }

    @Override
    public String toString() {
        return "Person{" +
               "addresses=" + addresses +
               ", id=" + id +
               ", firstName='" + firstName + '\'' +
               ", lastName='" + lastName + '\'' +
               ", age=" + age +
               ", mainAddress=" + mainAddress +
               ", phoneNumbers=" + phoneNumbers +
               ", hobbies=" + hobbies +
               '}';
    }
}
```

## `Address` class
<a name="ddb-en-client-adv-features-nested-address"></a>

```
@DynamoDbBean
public class Address {
    private String street;
    private String city;
    private String state;
    private String zipCode;

    public Address() {
    }

    public String getStreet() {
        return this.street;
    }

    public String getCity() {
        return this.city;
    }

    public String getState() {
        return this.state;
    }

    public String getZipCode() {
        return this.zipCode;
    }

    public void setStreet(String street) {
        this.street = street;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public void setState(String state) {
        this.state = state;
    }

    public void setZipCode(String zipCode) {
        this.zipCode = zipCode;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Address address = (Address) o;
        return Objects.equals(street, address.street) && Objects.equals(city, address.city) && Objects.equals(state, address.state) && Objects.equals(zipCode, address.zipCode);
    }

    @Override
    public int hashCode() {
        return Objects.hash(street, city, state, zipCode);
    }

    @Override
    public String toString() {
        return "Address{" +
                "street='" + street + '\'' +
                ", city='" + city + '\'' +
                ", state='" + state + '\'' +
                ", zipCode='" + zipCode + '\'' +
                '}';
    }
}
```

## `PhoneNumber` class
<a name="ddb-en-client-adv-features-nested-phonenumber"></a>

```
@DynamoDbBean
public class PhoneNumber {
    String type;
    String number;

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public String getNumber() {
        return number;
    }

    public void setNumber(String number) {
        this.number = number;
    }

    @Override
    public String toString() {
        return "PhoneNumber{" +
                "type='" + type + '\'' +
                ", number='" + number + '\'' +
                '}';
    }
}
```

## Save complex types
<a name="ddb-en-client-adv-features-nested-mapping"></a>

### Use annotated data classes
<a name="ddb-en-client-adv-features-nested-map-anno"></a>

You save nested attributes for custom classes by simply annotating them. The `Address` class and `PhoneNumber` class shown previously are annotated with only the `@DynamoDbBean` annotation. When the DynamoDB Enhanced Client API builds the table schema for the `Person` class with the following snippet, the API discovers the use of the `Address` and `PhoneNumber` classes and builds the corresponding mappings to work with DynamoDB.

```
TableSchema<Person> personTableSchema = TableSchema.fromBean(Person.class);
```

### Use abstract schemas with builders
<a name="ddb-en-client-adv-features-nested-map-builder"></a>

The alternative approach is to use static table schema builders for each nested bean class as shown in the following code.

The table schemas for the `Address` and `PhoneNumber` classes are abstract in the sense that they cannot be used with a DynamoDB table. This is because they lack definitions for the primary key. They are used, however, as nested schemas in the table schema for the `Person` class.

After comment lines 1 and 2 in the definition of `PERSON_TABLE_SCHEMA`, you see the code that uses the abstract table schemas. The use of `documentOf` in the `EnhanceType.documentOf(...)` method does not indicate that the method returns an `EnhancedDocument` type of the Enhanced Document API. The `documentOf(...)` method in this context returns an object that knows how to map its class argument to and from DynamoDB table attributes by using the table schema argument.

#### Static schema code
<a name="ddb-en-client-adv-features-nested-map-builder-code"></a>

```
    // Abstract table schema that cannot be used to work with a DynamoDB table,
    // but can be used as a nested schema.
    public static final TableSchema<Address> TABLE_SCHEMA_ADDRESS = TableSchema.builder(Address.class)
        .newItemSupplier(Address::new)
        .addAttribute(String.class, a -> a.name("street")
            .getter(Address::getStreet)
            .setter(Address::setStreet))
        .addAttribute(String.class, a -> a.name("city")
            .getter(Address::getCity)
            .setter(Address::setCity))
        .addAttribute(String.class, a -> a.name("zipcode")
            .getter(Address::getZipCode)
            .setter(Address::setZipCode))
        .addAttribute(String.class, a -> a.name("state")
            .getter(Address::getState)
            .setter(Address::setState))
        .build();

    // Abstract table schema that cannot be used to work with a DynamoDB table,
    // but can be used as a nested schema.
    public static final TableSchema<PhoneNumber> TABLE_SCHEMA_PHONENUMBER = TableSchema.builder(PhoneNumber.class)
        .newItemSupplier(PhoneNumber::new)
        .addAttribute(String.class, a -> a.name("type")
            .getter(PhoneNumber::getType)
            .setter(PhoneNumber::setType))
        .addAttribute(String.class, a -> a.name("number")
            .getter(PhoneNumber::getNumber)
            .setter(PhoneNumber::setNumber))
        .build();

    // A static table schema that can be used with a DynamoDB table.
    // The table schema contains two nested schemas that are used to perform mapping to/from DynamoDB.
    public static final TableSchema<Person> PERSON_TABLE_SCHEMA =
        TableSchema.builder(Person.class)
            .newItemSupplier(Person::new)
            .addAttribute(Integer.class, a -> a.name("id")
                .getter(Person::getId)
                .setter(Person::setId)
                .addTag(StaticAttributeTags.primaryPartitionKey()))
            .addAttribute(String.class, a -> a.name("firstName")
                .getter(Person::getFirstName)
                .setter(Person::setFirstName))
            .addAttribute(String.class, a -> a.name("lastName")
                .getter(Person::getLastName)
                .setter(Person::setLastName))
            .addAttribute(Integer.class, a -> a.name("age")
                .getter(Person::getAge)
                .setter(Person::setAge))
            .addAttribute(EnhancedType.documentOf(Address.class, TABLE_SCHEMA_ADDRESS), a -> a.name("mainAddress")
                .getter(Person::getMainAddress)
                .setter(Person::setMainAddress))
            .addAttribute(EnhancedType.listOf(String.class), a -> a.name("hobbies")
                .getter(Person::getHobbies)
                .setter(Person::setHobbies))
            .addAttribute(EnhancedType.mapOf(
                EnhancedType.of(String.class),
                // 1. Use mapping functionality of the Address table schema.
                EnhancedType.documentOf(Address.class, TABLE_SCHEMA_ADDRESS)), a -> a.name("addresses")
                .getter(Person::getAddresses)
                .setter(Person::setAddresses))
            .addAttribute(EnhancedType.listOf(
                // 2. Use mapping functionality of the PhoneNumber table schema.
                EnhancedType.documentOf(PhoneNumber.class, TABLE_SCHEMA_PHONENUMBER)), a -> a.name("phoneNumbers")
                .getter(Person::getPhoneNumbers)
                .setter(Person::setPhoneNumbers))
            .build();
```

## Project attributes of complex types
<a name="ddb-en-client-adv-features-nested-projection"></a>

For `query()` and `scan()` methods, you can specify which attributes you want to be returned in the results by using method calls such as `addNestedAttributeToProject()` and `attributesToProject()`. The DynamoDB Enhanced Client API converts the Java method call parameters into [projection expressions](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ProjectionExpressions.html) before the request is sent.

The following example populates the `Person` table with two items, then performs three scan operations. 

The first scan accesses all items in the table in order to compare the results to the other scan operations. 

The second scan uses the [https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/model/ScanEnhancedRequest.Builder.html#addNestedAttributeToProject(software.amazon.awssdk.enhanced.dynamodb.NestedAttributeName)](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/model/ScanEnhancedRequest.Builder.html#addNestedAttributeToProject(software.amazon.awssdk.enhanced.dynamodb.NestedAttributeName)) builder method to return only the `street` attribute value.

The third scan operation uses the [https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/model/ScanEnhancedRequest.Builder.html#attributesToProject(java.lang.String...)](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/model/ScanEnhancedRequest.Builder.html#attributesToProject(java.lang.String...)) builder method to return the data for the first-level attribute, `hobbies`. The attribute type of `hobbies` is a list. To access individual list items, perform a `get()` operation on the list.

```
        personDynamoDbTable = getDynamoDbEnhancedClient().table("Person", PERSON_TABLE_SCHEMA);
        PersonUtils.createPersonTable(personDynamoDbTable, getDynamoDbClient());
        // Use a utility class to add items to the Person table.
        List<Person> personList = PersonUtils.getItemsForCount(2);
        // This utility method performs a put against DynamoDB to save the instances in the list argument.
        PersonUtils.putCollection(getDynamoDbEnhancedClient(), personList, personDynamoDbTable);

        // The first scan logs all items in the table to compare to the results of the subsequent scans.
        final PageIterable<Person> allItems = personDynamoDbTable.scan();
        allItems.items().forEach(p ->
                // 1. Log what is in the table.
                logger.info(p.toString()));

        // Scan for nested attributes.
        PageIterable<Person> streetScanResult = personDynamoDbTable.scan(b -> b
                // Use the 'addNestedAttributeToProject()' or 'addNestedAttributesToProject()' to access data nested in maps in DynamoDB.
                .addNestedAttributeToProject(
                        NestedAttributeName.create("addresses", "work", "street")
                ));

        streetScanResult.items().forEach(p ->
                //2. Log the results of requesting nested attributes.
                logger.info(p.toString()));

        // Scan for a top-level list attribute.
        PageIterable<Person> hobbiesScanResult = personDynamoDbTable.scan(b -> b
                // Use the 'attributesToProject()' method to access first-level attributes.
                .attributesToProject("hobbies"));

        hobbiesScanResult.items().forEach((p) -> {
            // 3. Log the results of the request for the 'hobbies' attribute.
            logger.info(p.toString());
            // To access an item in a list, first get the parent attribute, 'hobbies', then access items in the list.
            String hobby = p.getHobbies().get(1);
            // 4. Log an item in the list.
            logger.info(hobby);
        });
```

```
// Logged results from comment line 1.
Person{id=2, firstName='first name 2', lastName='last name 2', age=11, addresses={work=Address{street='street 21', city='city 21', state='state 21', zipCode='33333'}, home=Address{street='street 2', city='city 2', state='state 2', zipCode='22222'}}, phoneNumbers=[PhoneNumber{type='home', number='222-222-2222'}, PhoneNumber{type='work', number='333-333-3333'}], hobbies=[hobby 2, hobby 21]}
Person{id=1, firstName='first name 1', lastName='last name 1', age=11, addresses={work=Address{street='street 11', city='city 11', state='state 11', zipCode='22222'}, home=Address{street='street 1', city='city 1', state='state 1', zipCode='11111'}}, phoneNumbers=[PhoneNumber{type='home', number='111-111-1111'}, PhoneNumber{type='work', number='222-222-2222'}], hobbies=[hobby 1, hobby 11]}

// Logged results from comment line 2.
Person{id=null, firstName='null', lastName='null', age=null, addresses={work=Address{street='street 21', city='null', state='null', zipCode='null'}}, phoneNumbers=null, hobbies=null}
Person{id=null, firstName='null', lastName='null', age=null, addresses={work=Address{street='street 11', city='null', state='null', zipCode='null'}}, phoneNumbers=null, hobbies=null}

// Logged results from comment lines 3 and 4.
Person{id=null, firstName='null', lastName='null', age=null, addresses=null, phoneNumbers=null, hobbies=[hobby 2, hobby 21]}
hobby 21
Person{id=null, firstName='null', lastName='null', age=null, addresses=null, phoneNumbers=null, hobbies=[hobby 1, hobby 11]}
hobby 11
```

**Note**  
If the `attributesToProject()` method follows any other builder method that adds attributes that you want to project, the list of attribute names supplied to the `attributesToProject()` replaces all other attribute names.  
A scan performed with the `ScanEnhancedRequest` instance in the following snippet returns only hobby data.  

```
ScanEnhancedRequest lastOverwrites = ScanEnhancedRequest.builder()
        .addNestedAttributeToProject(
                NestedAttributeName.create("addresses", "work", "street"))
        .addAttributeToProject("firstName")
        // If the 'attributesToProject()' method follows other builder methods that add attributes for projection,
        // its list of attributes replace all previous attributes.
        .attributesToProject("hobbies")
        .build();
PageIterable<Person> hobbiesOnlyResult = personDynamoDbTable.scan(lastOverwrites);
hobbiesOnlyResult.items().forEach(p ->
        logger.info(p.toString()));

// Logged results.
Person{id=null, firstName='null', lastName='null', age=null, addresses=null, phoneNumbers=null, hobbies=[hobby 2, hobby 21]}
Person{id=null, firstName='null', lastName='null', age=null, addresses=null, phoneNumbers=null, hobbies=[hobby 1, hobby 11]}
```
The following code snippet uses the `attributesToProject()` method first. This ordering preserves all other requested attributes.  

```
ScanEnhancedRequest attributesPreserved = ScanEnhancedRequest.builder()
        // Use 'attributesToProject()' first so that the method call does not replace all other attributes
        // that you want to project.
        .attributesToProject("firstName")
        .addNestedAttributeToProject(
                NestedAttributeName.create("addresses", "work", "street"))
        .addAttributeToProject("hobbies")
        .build();
PageIterable<Person> allAttributesResult = personDynamoDbTable.scan(attributesPreserved);
allAttributesResult.items().forEach(p ->
        logger.info(p.toString()));

// Logged results.
Person{id=null, firstName='first name 2', lastName='null', age=null, addresses={work=Address{street='street 21', city='null', state='null', zipCode='null'}}, phoneNumbers=null, hobbies=[hobby 2, hobby 21]}
Person{id=null, firstName='first name 1', lastName='null', age=null, addresses={work=Address{street='street 11', city='null', state='null', zipCode='null'}}, phoneNumbers=null, hobbies=[hobby 1, hobby 11]}
```

## Use complex types in expressions
<a name="ddb-en-client-adv-features-nested-expressions"></a>

You can use complex types in expressions, such as filter expressions and condition expressions, by using dereferencing operators to navigate the structure of the complex type. For objects and maps, use the `. (dot)` and for list elements use `[n]` (square brackets around the sequence number of the element). You can't refer to individual elements of a set, but you can use the [`contains` function](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html#Expressions.OperatorsAndFunctions.Functions).

The following example show two filter expressions that are used in scan operations. The filter expressions specify the match conditions for items you want in the results. The example uses `Person`, `Address`, and `PhoneNumber` classes shown previously.

```
    public void scanUsingFilterOfNestedAttr() {
        // The following is a filter expression for an attribute that is a map of Address objects.
        // By using this filter expression, the SDK returns Person objects that have an address
        // with 'mailing' as a key and 'MS2' for a state value.
        Expression addressFilter = Expression.builder()
                .expression("addresses.#type.#field = :value")
                .putExpressionName("#type", "mailing")
                .putExpressionName("#field", "state")
                .putExpressionValue(":value", AttributeValue.builder().s("MS2").build())
                .build();

        PageIterable<Person> addressFilterResults = personDynamoDbTable.scan(rb -> rb.
                filterExpression(addressFilter));
        addressFilterResults.items().stream().forEach(p -> logger.info("Person: {}", p));

        assert addressFilterResults.items().stream().count() == 1;


        // The following is a filter expression for an attribute that is a list of phone numbers.
        // By using this filter expression, the SDK returns Person objects whose second phone number
        // in the list has a type equal to 'cell'.
        Expression phoneFilter = Expression.builder()
                .expression("phoneNumbers[1].#type = :type")
                .putExpressionName("#type", "type")
                .putExpressionValue(":type", AttributeValue.builder().s("cell").build())
                .build();

        PageIterable<Person> phoneFilterResults = personDynamoDbTable.scan(rb -> rb
                .filterExpression(phoneFilter)
                .attributesToProject("id", "firstName", "lastName", "phoneNumbers")
        );

        phoneFilterResults.items().stream().forEach(p -> logger.info("Person: {}", p));

        assert phoneFilterResults.items().stream().count() == 1;
        assert phoneFilterResults.items().stream().findFirst().get().getPhoneNumbers().get(1).getType().equals("cell");
    }
```

### Helper method that populates the table
<a name="nested-expressions-helper-method"></a>

```
    public static void populateDatabase() {
        Person person1 = new Person();
        person1.setId(1);
        person1.setFirstName("FirstName1");
        person1.setLastName("LastName1");

        Address billingAddr1 = new Address();
        billingAddr1.setState("BS1");
        billingAddr1.setCity("BillingTown1");

        Address mailing1 = new Address();
        mailing1.setState("MS1");
        mailing1.setCity("MailingTown1");

        person1.setAddresses(Map.of("billing", billingAddr1, "mailing", mailing1));

        PhoneNumber pn1_1 = new PhoneNumber();
        pn1_1.setType("work");
        pn1_1.setNumber("111-111-1111");

        PhoneNumber pn1_2 = new PhoneNumber();
        pn1_2.setType("home");
        pn1_2.setNumber("222-222-2222");

        List<PhoneNumber> phoneNumbers1 = List.of(pn1_1, pn1_2);
        person1.setPhoneNumbers(phoneNumbers1);

        personDynamoDbTable.putItem(person1);

        Person person2 = person1;
        person2.setId(2);
        person2.setFirstName("FirstName2");
        person2.setLastName("LastName2");

        Address billingAddress2 = billingAddr1;
        billingAddress2.setCity("BillingTown2");
        billingAddress2.setState("BS2");

        Address mailing2 = mailing1;
        mailing2.setCity("MailingTown2");
        mailing2.setState("MS2");

        person2.setAddresses(Map.of("billing", billingAddress2, "mailing", mailing2));

        PhoneNumber pn2_1 = new PhoneNumber();
        pn2_1.setType("work");
        pn2_1.setNumber("333-333-3333");

        PhoneNumber pn2_2 = new PhoneNumber();
        pn2_2.setType("cell");
        pn2_2.setNumber("444-444-4444");

        List<PhoneNumber> phoneNumbers2 = List.of(pn2_1, pn2_2);
        person2.setPhoneNumbers(phoneNumbers2);

        personDynamoDbTable.putItem(person2);
    }
```

### JSON representation of items in the database
<a name="nested-attributes-expression-json-items"></a>

```
{
 "id": 1,
 "addresses": {
  "billing": {
   "city": "BillingTown1",
   "state": "BS1",
   "street": null,
   "zipCode": null
  },
  "mailing": {
   "city": "MailingTown1",
   "state": "MS1",
   "street": null,
   "zipCode": null
  }
 },
 "firstName": "FirstName1",
 "lastName": "LastName1",
 "phoneNumbers": [
  {
   "number": "111-111-1111",
   "type": "work"
  },
  {
   "number": "222-222-2222",
   "type": "home"
  }
 ]
}

{
 "id": 2,
 "addresses": {
  "billing": {
   "city": "BillingTown2",
   "state": "BS2",
   "street": null,
   "zipCode": null
  },
  "mailing": {
   "city": "MailingTown2",
   "state": "MS2",
   "street": null,
   "zipCode": null
  }
 },
 "firstName": "FirstName2",
 "lastName": "LastName2",
 "phoneNumbers": [
  {
   "number": "333-333-3333",
   "type": "work"
  },
  {
   "number": "444-444-4444",
   "type": "cell"
  }
 ]
}
```

## Update items that contain complex types
<a name="ddb-en-client-adv-features-nested-updates"></a>

To update an item that contains complex types, you have two basic approaches:
+ Approach 1: First retrieve the item (by using `getItem`), update the object, then call `DynamoDbTable#updateItem`.
+ Approach 2: Don't retrieve the item, but construct a new instance, set the properties you want to update, and submit the instance to `DynamoDbTable#updateItem` by setting the appropriate value of [https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/model/IgnoreNullsMode.html](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/model/IgnoreNullsMode.html) . This approach does not require that you fetch the item before updating it.

The examples shown in this section use the `Person`, `Address`, and `PhoneNumber` classes shown previously.

### Update approach 1: retrieve, then update
<a name="ddb-en-client-adv-features-nested-updates-retreive"></a>

By using this approach, you ensure that no data is lost on update. The DynamoDB Enhanced Client API recreates the bean with the attributes from the item saved in DynamoDB including values of complex types. You then need to use the getters and setters to update the bean. The downside of this approach is the cost you incur retrieving the item first.

The following example demonstrates that no data is lost if you first retrieve the item before updating it.

```
    public void retrieveThenUpdateExample()  {
        // Assume that we ran this code yesterday.
        Person person = new Person();
        person.setId(1);
        person.setFirstName("FirstName");
        person.setLastName("LastName");

        Address mainAddress = new Address();
        mainAddress.setStreet("123 MyStreet");
        mainAddress.setCity("MyCity");
        mainAddress.setState("MyState");
        mainAddress.setZipCode("MyZipCode");
        person.setMainAddress(mainAddress);

        PhoneNumber homePhone = new PhoneNumber();
        homePhone.setNumber("1111111");
        homePhone.setType("HOME");
        person.setPhoneNumbers(List.of(homePhone));

        personDynamoDbTable.putItem(person);

        // Assume that we are running this code now.
        // First, retrieve the item
        Person retrievedPerson = personDynamoDbTable.getItem(Key.builder().partitionValue(1).build());

        // Make any updates.
        retrievedPerson.getMainAddress().setCity("YourCity");

        // Save the updated bean. 'updateItem' returns the bean as it appears after the update.
        Person updatedPerson = personDynamoDbTable.updateItem(retrievedPerson);

        // Verify for this example.
        Address updatedMainAddress = updatedPerson.getMainAddress();
        assert updatedMainAddress.getCity().equals("YourCity");
        assert updatedMainAddress.getState().equals("MyState"); // Unchanged.
        // The list of phone numbers remains; it was not set to null;
        assert updatedPerson.getPhoneNumbers().size() == 1;
    }
```

### Update approach 2: Use an `IgnoreNullsMode` enum without retrieving the item first
<a name="ddb-en-client-adv-features-nested-updates-nullmode"></a>

To update an item in DynamoDB, you can provide a new object that has only the properties that you want updated and leave the other values as null. With this approach, you need to be aware of how null values in the object are treated by the SDK and how you can control the behavior.

To specify which null-valued properties you want the SDK to ignore, provide an `IgnoreNullsMode` enum when you build the [https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/model/UpdateItemEnhancedRequest.Builder.html](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/model/UpdateItemEnhancedRequest.Builder.html). As an example of using one of the enumerated values, the following snippet uses the `IgnoreNullsMode.SCALAR_ONLY` mode.

```
// Create a new Person object to update the existing item in DynamoDB.
Person personForUpdate = new Person();
personForUpdate.setId(1);
personForUpdate.setFirstName("updatedFirstName");  // 'firstName' is a top scalar property.

Address addressForUpdate = new Address();
addressForUpdate.setCity("updatedCity");
personForUpdate.setMainAddress(addressForUpdate);

personDynamoDbTable.updateItem(r -> r
                .item(personForUpdate)
                .ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY));

/* With IgnoreNullsMode.SCALAR_ONLY provided, The SDK ignores all null properties. The SDK adds or replaces
the 'firstName' property with the provided value, "updatedFirstName". The SDK updates the 'city' value of
'mainAddress', as long as the 'mainAddress' attribute already exists in DynamoDB.

In the background, the SDK generates an update expression that it sends in the request to DynamoDB.
The following JSON object is a simplified version of what it sends. Notice that the SDK includes the paths
to 'mainAddress.city' and 'firstName' in the SET clause of the update expression. No null values in
'personForUpdate' are included.

{
  "TableName": "PersonTable",
  "Key": {
    "id": {
      "N": "1"
    }
  },
  "ReturnValues": "ALL_NEW",
  "UpdateExpression": "SET #mainAddress.#city = :mainAddress_city, #firstName = :firstName",
  "ExpressionAttributeNames": {
    "#city": "city",
    "#firstName": "firstName",
    "#mainAddress": "mainAddress"
  },
  "ExpressionAttributeValues": {
    ":firstName": {
      "S": "updatedFirstName"
    },
    ":mainAddress_city": {
      "S": "updatedCity"
    }
  }
}

Had we chosen 'IgnoreNullsMode.DEFAULT' instead of 'IgnoreNullsMode.SCALAR_ONLY', the SDK would have included
null values in the "ExpressionAttributeValues" section of the request as shown in the following snippet.

  "ExpressionAttributeValues": {
    ":mainAddress": {
      "M": {
        "zipCode": {
          "NULL": true
        },
        "city": {
          "S": "updatedCity"
        },
        "street": {
          "NULL": true
        },
        "state": {
          "NULL": true
        }
      }
    },
    ":firstName": {
      "S": "updatedFirstName"
    }
  }
*/
```

The Amazon DynamoDB Developer Guide contains more information about [update expressions](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html).

#### Descriptions of the `IgnoreNullsMode` options
<a name="ignore-nulls-mode-descriptions"></a>
+ `IgnoreNullsMode.SCALAR_ONLY` - Use this setting to update scalar attributes at any level. The SDK constructs an update statement that sends only non-null, scalar attributes to DynamoDB. The SDK ignores null-valued, scalar attributes of a bean or map, retaining the saved value in DynamoDB.

  When you update a scalar attribute of map or bean, the map must already exists in DynamoDB. If you add a map or a bean to the object that does not already exist for the object in DynamoDB, you get a `DynamoDbException` with the message *The document path provided in the update expression is invalid for update*. You must use `MAPS_ONLY` mode to add a bean or map to DynamoDB before you update any of its attributes.
+ `IgnoreNullsMode.MAPS_ONLY` - Use this setting to add or replace properties that are a bean or a map. The SDK replaces or adds any map or bean provided in the object. Any beans or maps that are null in the object are ignored, retaining the map that exists in DynamoDB.
+ `IgnoreNullsMode.DEFAULT` - With this setting, the SDK never ignores null values. Scalar attributes at any level that are null are updated to null. The SDK updates any null-valued bean, map, list, or set property in the object to null in DynamoDB. When you use this mode—or don't provide a mode since it's the default mode—you should retrieve the item first so that values in DynamoDB are not set to null that are provided in the object for updating, unless your intention is to set the values to null.

In all modes, if you provide an object to `updateItem` that has a non-null list or set, the list or set is saved to DynamoDB. 

#### Why the modes?
<a name="ddb-en-client-adv-features-nested-updates-nullmodes-why"></a>

When you provide an object with a bean or map to the `updateItem` method, the SDK can't tell if it should use the property values in the bean (or entry values in the map) to update the item, or if the entire bean/map should replace what's been saved to DynamoDB.

Working from our previous example that shows the retrieval of the item first, let's attempt to update the `city` attribute of `mainAddress` without the retrieval.

```
/* The retrieval example saved the Person object with a 'mainAddress' property whose 'city' property value is "MyCity".
/* Note that we create a new Person with only the necessary information to update the city value
of the mainAddress. */
Person personForUpdate = new Person();
personForUpdate.setId(1);
// The update we want to make changes the city.
Address mainAddressForUpdate = new Address();
mainAddressForUpdate.setCity("YourCity");
personForUpdate.setMainAddress(mainAddressForUpdate);

// Lets' try the following:
Person updatedPerson = personDynamoDbTable.updateItem(personForUpdate);
/*
 Since we haven't retrieved the item, we don't know if the 'mainAddress' property
 already exists, so what update expression should the SDK generate?

A) Should it replace or add the 'mainAddress' with the provided object (setting all attributes to null other than city)
   as shown in the following simplified JSON?

      {
        "TableName": "PersonTable",
        "Key": {
          "id": {
            "N": "1"
          }
        },
        "ReturnValues": "ALL_NEW",
        "UpdateExpression": "SET #mainAddress = :mainAddress",
        "ExpressionAttributeNames": {
          "#mainAddress": "mainAddress"
        },
        "ExpressionAttributeValues": {
          ":mainAddress": {
            "M": {
              "zipCode": {
                "NULL": true
              },
              "city": {
                "S": "YourCity"
              },
              "street": {
                "NULL": true
              },
              "state": {
                "NULL": true
              }
            }
          }
        }
      }
 
B) Or should it update only the 'city' attribute of an existing 'mainAddress' as shown in the following simplified JSON?

      {
        "TableName": "PersonTable",
        "Key": {
          "id": {
            "N": "1"
          }
        },
        "ReturnValues": "ALL_NEW",
        "UpdateExpression": "SET #mainAddress.#city = :mainAddress_city",
        "ExpressionAttributeNames": {
          "#city": "city",
          "#mainAddress": "mainAddress"
        },
        "ExpressionAttributeValues": {
          ":mainAddress_city": {
            "S": "YourCity"
          }
        }
      }

However, assume that we don't know if the 'mainAddress' already exists. If it doesn't exist, the SDK would try to update 
an attribute of a non-existent map, which results in an exception.

In this particular case, we would likely select option B (SCALAR_ONLY) to retain the other values of the 'mainAddress'.
*/
```

The following two examples show uses of the `MAPS_ONLY` and `SCALAR_ONLY` enumerated values. `MAPS_ONLY` adds a map and `SCALAR_ONLY` updates a map.

##### `IgnoreNullsMode.MAPS_ONLY` example
<a name="scalar-only-example"></a>

```
    public void mapsOnlyModeExample() {
        // Assume that we ran this code yesterday.
        Person person = new Person();
        person.setId(1);
        person.setFirstName("FirstName");

        personDynamoDbTable.putItem(person);

        // Assume that we are running this code now.

        /* Note that we create a new Person with only the necessary information to update the city value
        of the mainAddress. */
        Person personForUpdate = new Person();
        personForUpdate.setId(1);
        // The update we want to make changes the city.
        Address mainAddressForUpdate = new Address();
        mainAddressForUpdate.setCity("YourCity");
        personForUpdate.setMainAddress(mainAddressForUpdate);


        Person updatedPerson = personDynamoDbTable.updateItem(r -> r
                .item(personForUpdate)
                .ignoreNullsMode(IgnoreNullsMode.MAPS_ONLY)); // Since the mainAddress property does not exist, use MAPS_ONLY mode.
        assert updatedPerson.getMainAddress().getCity().equals("YourCity");
        assert updatedPerson.getMainAddress().getState() == null;
    }
```

##### `IgnoreNullsMode.SCALAR_ONLY example`
<a name="maps-only-example"></a>

```
    public void scalarOnlyExample() {
        // Assume that we ran this code yesterday.
        Person person = new Person();
        person.setId(1);
        Address mainAddress = new Address();
        mainAddress.setCity("MyCity");
        mainAddress.setState("MyState");
        person.setMainAddress(mainAddress);

        personDynamoDbTable.putItem(person);

        // Assume that we are running this code now.

        /* Note that we create a new Person with only the necessary information to update the city value
        of the mainAddress. */
        Person personForUpdate = new Person();
        personForUpdate.setId(1);
        // The update we want to make changes the city.
        Address mainAddressForUpdate = new Address();
        mainAddressForUpdate.setCity("YourCity");
        personForUpdate.setMainAddress(mainAddressForUpdate);

        Person updatedPerson = personDynamoDbTable.updateItem(r -> r
                .item(personForUpdate)
                .ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); // SCALAR_ONLY mode ignores null properties in the in mainAddress.
        assert updatedPerson.getMainAddress().getCity().equals("YourCity");
        assert updatedPerson.getMainAddress().getState().equals("MyState"); // The state property remains the same.
    }
```

Refer to the following table to see which null values are ignored for each mode. You can often work with either `SCALAR_ONLY` and `MAPS_ONLY` except when you work with beans or maps.


**Which null-valued properties in the object submitted to `updateItem` does the SDK ignore for each mode?**  

| Type of property | in SCALAR\$1ONLY mode | in MAPS\$1ONLY mode | in DEFAULT mode | 
| --- | --- | --- | --- | 
| Top scalar | Yes | Yes | No | 
| Bean or map | Yes | Yes | No | 
| Scalar value of a bean or map entry | Yes1 | No2 | No | 
| List or set | Yes | Yes | No | 

1This assumes the map already exists in DynamoDB. Any scalar value—null or not null—of the bean or map that you provide in the object for update requires that a path to the value exists in DynamoDB. The SDK constructs a path to the attribute by using the `. (dot)` dereference operator before it submits the request.

2Since you use `MAPS_ONLY` mode to fully replace or to add a bean or map, all null values in the bean or map are retained in the map saved to DynamoDB.

# Preserve empty objects with `@DynamoDbPreserveEmptyObject`
<a name="ddb-en-client-adv-features-empty"></a>

If you save a bean to Amazon DynamoDB with empty objects and you want the SDK to recreate the empty objects upon retrieval, annotate the getter of the inner bean with `@DynamoDbPreserveEmptyObject`.

To illustrate how the annotation works, the code example uses the following two beans.

## Example beans
<a name="ddb-en-client-adv-features-empty-ex1"></a>

The following data class contains two `InnerBean` fields. The getter method, `getInnerBeanWithoutAnno()`, is not annotated with `@DynamoDbPreserveEmptyObject`. The `getInnerBeanWithAnno()` method is annotated.

```
@DynamoDbBean
public class MyBean {

    private String id;
    private String name;
    private InnerBean innerBeanWithoutAnno;
    private InnerBean innerBeanWithAnno;

    @DynamoDbPartitionKey
    public String getId() { return id; }
    public void setId(String id) { this.id = id; }

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public InnerBean getInnerBeanWithoutAnno() { return innerBeanWithoutAnno; }
    public void setInnerBeanWithoutAnno(InnerBean innerBeanWithoutAnno) { this.innerBeanWithoutAnno = innerBeanWithoutAnno; }

    @DynamoDbPreserveEmptyObject
    public InnerBean getInnerBeanWithAnno() { return innerBeanWithAnno; }
    public void setInnerBeanWithAnno(InnerBean innerBeanWithAnno) { this.innerBeanWithAnno = innerBeanWithAnno; }

    @Override
    public String toString() {
        return new StringJoiner(", ", MyBean.class.getSimpleName() + "[", "]")
                .add("innerBeanWithoutAnno=" + innerBeanWithoutAnno)
                .add("innerBeanWithAnno=" + innerBeanWithAnno)
                .add("id='" + id + "'")
                .add("name='" + name + "'")
                .toString();
    }
}
```

Instances of the following `InnerBean` class are fields of `MyBean` and are initialized as empty objects in the example code.

```
@DynamoDbBean
public class InnerBean {

    private String innerBeanField;

    public String getInnerBeanField() {
        return innerBeanField;
    }

    public void setInnerBeanField(String innerBeanField) {
        this.innerBeanField = innerBeanField;
    }

    @Override
    public String toString() {
        return "InnerBean{" +
                "innerBeanField='" + innerBeanField + '\'' +
                '}';
    }
}
```

The following code example saves a `MyBean` object with initialized inner beans to DynamoDB and then retrieves the item. The logged output shows that the `innerBeanWithoutAnno` is not initialized, but `innerBeanWithAnno` has been created.

```
    public MyBean preserveEmptyObjectAnnoUsingGetItemExample(DynamoDbTable<MyBean> myBeanTable) {
        // Save an item to DynamoDB.
        MyBean bean = new MyBean();
        bean.setId("1");
        bean.setInnerBeanWithoutAnno(new InnerBean());   // Instantiate the inner bean.
        bean.setInnerBeanWithAnno(new InnerBean());      // Instantiate the inner bean.
        myBeanTable.putItem(bean);

        GetItemEnhancedRequest request = GetItemEnhancedRequest.builder()
                .key(Key.builder().partitionValue("1").build())
                .build();
        MyBean myBean = myBeanTable.getItem(request);

        logger.info(myBean.toString());
        // Output 'MyBean[innerBeanWithoutAnno=null, innerBeanWithAnno=InnerBean{innerBeanField='null'}, id='1', name='null']'.

        return myBean;
    }
```

## Alternative static schema
<a name="ddb-en-client-adv-features-empty-ex1-static"></a>

You can use the following `StaticTableSchema` version of the table schemas in place of the annotations on the beans.

```
    public static TableSchema<MyBean> buildStaticSchemas() {

        StaticTableSchema<InnerBean> innerBeanStaticTableSchema =
                StaticTableSchema.builder(InnerBean.class)
                        .newItemSupplier(InnerBean::new)
                        .addAttribute(String.class, a -> a.name("innerBeanField")
                                .getter(InnerBean::getInnerBeanField)
                                .setter(InnerBean::setInnerBeanField))
                        .build();

        return StaticTableSchema.builder(MyBean.class)
                .newItemSupplier(MyBean::new)
                .addAttribute(String.class, a -> a.name("id")
                        .getter(MyBean::getId)
                        .setter(MyBean::setId)
                        .addTag(primaryPartitionKey()))
                .addAttribute(String.class, a -> a.name("name")
                        .getter(MyBean::getName)
                        .setter(MyBean::setName))
                .addAttribute(EnhancedType.documentOf(InnerBean.class,
                                innerBeanStaticTableSchema),
                        a -> a.name("innerBean1")
                                .getter(MyBean::getInnerBeanWithoutAnno)
                                .setter(MyBean::setInnerBeanWithoutAnno))
                .addAttribute(EnhancedType.documentOf(InnerBean.class,
                                innerBeanStaticTableSchema,
                                b -> b.preserveEmptyObject(true)),
                        a -> a.name("innerBean2")
                                .getter(MyBean::getInnerBeanWithAnno)
                                .setter(MyBean::setInnerBeanWithAnno))
                .build();
    }
```

# Avoid saving null attributes of nested objects
<a name="ddb-en-client-adv-features-ignore-null"></a>

You can skip null attributes of nested objects when saving a data class object to DynamoDB by applying the `@DynamoDbIgnoreNulls` annotation. By contrast, top-level attributes with null values are never saved to the database.

To illustrate how the annotation works, the code example uses the following two beans.

## Example beans
<a name="ddb-en-client-adv-features-ignore-null-ex1"></a>

The following data class contains two `InnerBean` fields. The getter method, `getInnerBeanWithoutAnno()`, is not annotated. The `getInnerBeanWithIgnoreNullsAnno()` method is annotated with `@DynamoDbIgnoreNulls`.

```
@DynamoDbBean
public class MyBean {

    private String id;
    private String name;
    private InnerBean innerBeanWithoutAnno;
    private InnerBean innerBeanWithIgnoreNullsAnno;

    @DynamoDbPartitionKey
    public String getId() { return id; }
    public void setId(String id) { this.id = id; }

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public InnerBean getInnerBeanWithoutAnno() { return innerBeanWithoutAnno; }
    public void setInnerBeanWithoutAnno(InnerBean innerBeanWithoutAnno) { this.innerBeanWithoutAnno = innerBeanWithoutAnno; }

    @DynamoDbIgnoreNulls
    public InnerBean getInnerBeanWithIgnoreNullsAnno() { return innerBeanWithIgnoreNullsAnno; }
    public void setInnerBeanWithIgnoreNullsAnno(InnerBean innerBeanWithAnno) { this.innerBeanWithIgnoreNullsAnno = innerBeanWithAnno; }

    @Override
    public String toString() {
        return new StringJoiner(", ", MyBean.class.getSimpleName() + "[", "]")
                .add("innerBeanWithoutAnno=" + innerBeanWithoutAnno)
                .add("innerBeanWithIgnoreNullsAnno=" + innerBeanWithIgnoreNullsAnno)
                .add("id='" + id + "'")
                .add("name='" + name + "'")
                .toString();
    }
}
```

Instances of the following `InnerBean` class are fields of `MyBean` and are used in the following example code.

```
@DynamoDbBean
public class InnerBean {

    private String innerBeanFieldString;
    private Integer innerBeanFieldInteger;

    public String getInnerBeanFieldString() { return innerBeanFieldString; }
    public void setInnerBeanFieldString(String innerBeanFieldString) { this.innerBeanFieldString = innerBeanFieldString; }

    public Integer getInnerBeanFieldInteger() { return innerBeanFieldInteger; }
    public void setInnerBeanFieldInteger(Integer innerBeanFieldInteger) { this.innerBeanFieldInteger = innerBeanFieldInteger; }

    @Override
    public String toString() {
        return new StringJoiner(", ", InnerBean.class.getSimpleName() + "[", "]")
                .add("innerBeanFieldString='" + innerBeanFieldString + "'")
                .add("innerBeanFieldInteger=" + innerBeanFieldInteger)
                .toString();
    }
}
```

The following code example creates an `InnerBean` object and sets only one of its two attributes with a value. 

```
    public void ignoreNullsAnnoUsingPutItemExample(DynamoDbTable<MyBean> myBeanTable) {
        // Create an InnerBean object and give only one attribute a value.
        InnerBean innerBeanOneAttributeSet = new InnerBean();
        innerBeanOneAttributeSet.setInnerBeanFieldInteger(200);

        // Create a MyBean instance and use the same InnerBean instance both for attributes.
        MyBean bean = new MyBean();
        bean.setId("1");
        bean.setInnerBeanWithoutAnno(innerBeanOneAttributeSet);
        bean.setInnerBeanWithIgnoreNullsAnno(innerBeanOneAttributeSet);

        Map<String, AttributeValue> itemMap = myBeanTable.tableSchema().itemToMap(bean, true);
        logger.info(itemMap.toString());
        // Log the map that is sent to the database.
        // {innerBeanWithIgnoreNullsAnno=AttributeValue(M={innerBeanFieldInteger=AttributeValue(N=200)}), id=AttributeValue(S=1), innerBeanWithoutAnno=AttributeValue(M={innerBeanFieldInteger=AttributeValue(N=200), innerBeanFieldString=AttributeValue(NUL=true)})}
        
        // Save the MyBean object to the table.
        myBeanTable.putItem(bean);
    }
```

To visualize the low-level data that is sent to DynamoDB, the code logs the attribute map before saving the `MyBean` object.

The logged output shows that the `innerBeanWithIgnoreNullsAnno` outputs one attribute,

```
innerBeanWithIgnoreNullsAnno=AttributeValue(M={innerBeanFieldInteger=AttributeValue(N=200)})
```

The `innerBeanWithoutAnno` instance outputs two attributes. One attribute has a value of 200 and the other is a null-valued attribute.

```
innerBeanWithoutAnno=AttributeValue(M={innerBeanFieldInteger=AttributeValue(N=200), innerBeanFieldString=AttributeValue(NUL=true)})
```

## JSON representation of the attribute map
<a name="ddb-en-client-adv-features-ignore-null-ex2"></a>

The following JSON representation makes it easier to see the data that is saved to DynamoDB.

```
{
  "id": {
    "S": "1"
  },
  "innerBeanWithIgnoreNullsAnno": {
    "M": {
      "innerBeanFieldInteger": {
        "N": "200"
      }
    }
  },
  "innerBeanWithoutAnno": {
    "M": {
      "innerBeanFieldInteger": {
        "N": "200"
      },
      "innerBeanFieldString": {
        "NULL": true
      }
    }
  }
}
```

## Alternative static schema
<a name="ddb-en-client-adv-features-empty-ex1-static"></a>

You can use the following `StaticTableSchema` version of the table schemas in place data class annotations.

```
public static TableSchema<MyBean> buildStaticSchemas() {

    StaticTableSchema<InnerBean> innerBeanStaticTableSchema =
        StaticTableSchema.builder(InnerBean.class)
            .newItemSupplier(InnerBean::new)
            .addAttribute(String.class, a -> a.name("innerBeanFieldString")
                .getter(InnerBean::getInnerBeanFieldString)
                .setter(InnerBean::setInnerBeanFieldString))
            .addAttribute(Integer.class, a -> a.name("innerBeanFieldInteger")
                .getter(InnerBean::getInnerBeanFieldInteger)
                .setter(InnerBean::setInnerBeanFieldInteger))
            .build();

    return StaticTableSchema.builder(MyBean.class)
        .newItemSupplier(MyBean::new)
        .addAttribute(String.class, a -> a.name("id")
            .getter(MyBean::getId)
            .setter(MyBean::setId)
            .addTag(primaryPartitionKey()))
        .addAttribute(String.class, a -> a.name("name")
            .getter(MyBean::getName)
            .setter(MyBean::setName))
        .addAttribute(EnhancedType.documentOf(InnerBean.class,
                innerBeanStaticTableSchema),
            a -> a.name("innerBeanWithoutAnno")
                .getter(MyBean::getInnerBeanWithoutAnno)
                .setter(MyBean::setInnerBeanWithoutAnno))
        .addAttribute(EnhancedType.documentOf(InnerBean.class,
                innerBeanStaticTableSchema,
                b -> b.ignoreNulls(true)),
            a -> a.name("innerBeanWithIgnoreNullsAnno")
                .getter(MyBean::getInnerBeanWithIgnoreNullsAnno)
                .setter(MyBean::setInnerBeanWithIgnoreNullsAnno))
        .build();
}
```