Skip to content

Immutables support in DynamoDb Enhanced Client #2012

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 2, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "feature",
"category": "DynamoDB Enhanced Client",
"description": "Support for mapping to and from immutable Java objects using ImmutableTableSchema and StaticImmutableTableSchema."
}
105 changes: 98 additions & 7 deletions services-custom/dynamodb-enhanced/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ values used are also completely arbitrary.
}
```

2. Create a TableSchema for your class. For this example we are using the 'bean' TableSchema that will scan your bean
class and use the annotations to infer the table structure and attributes :
2. Create a TableSchema for your class. For this example we are using a static constructor method on TableSchema that
will scan your annotated class and infer the table structure and attributes :
```java
static final TableSchema<Customer> CUSTOMER_TABLE_SCHEMA = TableSchema.fromBean(Customer.class);
static final TableSchema<Customer> CUSTOMER_TABLE_SCHEMA = TableSchema.fromClass(Customer.class);
```

If you would prefer to skip the slightly costly bean inference for a faster solution, you can instead declare your
Expand Down Expand Up @@ -155,6 +155,96 @@ index. Here's an example of how to do this:
PageIterable<Customer> customersWithName =
customersByName.query(r -> r.queryConditional(equalTo(k -> k.partitionValue("Smith"))));
```

### Working with immutable data classes
It is possible to have the DynamoDB Enhanced Client map directly to and from immutable data classes in Java. An
immutable class is expected to only have getters and will also be associated with a separate builder class that
is used to construct instances of the immutable data class. The DynamoDB annotation style for immutable classes is
very similar to bean classes :

```java
@DynamoDbImmutable(builder = Customer.Builder.class)
public class Customer {
private final String accountId;
private final int subId;
private final String name;
private final Instant createdDate;

private Customer(Builder b) {
this.accountId = b.accountId;
this.subId = b.subId;
this.name = b.name;
this.createdDate = b.createdDate;
}

// This method will be automatically discovered and used by the TableSchema
public static Builder builder() { return new Builder(); }

@DynamoDbPartitionKey
public String accountId() { return this.accountId; }

@DynamoDbSortKey
public int subId() { return this.subId; }

@DynamoDbSecondaryPartitionKey(indexNames = "customers_by_name")
public String name() { return this.name; }

@DynamoDbSecondarySortKey(indexNames = {"customers_by_date", "customers_by_name"})
public Instant createdDate() { return this.createdDate; }

public static final class Builder {
private String accountId;
private int subId;
private String name;
private Instant createdDate;

private Builder() {}

public Builder accountId(String accountId) { this.accountId = accountId; return this; }
public Builder subId(int subId) { this.subId = subId; return this; }
public Builder name(String name) { this.name = name; return this; }
public Builder createdDate(Instant createdDate) { this.createdDate = createdDate; return this; }

// This method will be automatically discovered and used by the TableSchema
public Customer build() { return new Customer(this); }
}
}
```

The following requirements must be met for a class annotated with @DynamoDbImmutable:
1. Every method on the immutable class that is not an override of Object.class or annotated with @DynamoDbIgnore must
be a getter for an attribute of the database record.
1. Every getter in the immutable class must have a corresponding setter on the builder class that has a case-sensitive
matching name.
1. EITHER: the builder class must have a public default constructor; OR: there must be a public static method named
'builder' on the immutable class that takes no parameters and returns an instance of the builder class.
1. The builder class must have a public method named 'build' that takes no parameters and returns an instance of the
immutable class.

There are third-party library that help generate a lot of the boilerplate code associated with immutable objects.
The DynamoDb Enhanced client should work with these libraries as long as they follow the conventions detailed
in this section. Here's an example of the immutable Customer class using Lombok with DynamoDb annotations (note
how Lombok's 'onMethod' feature is leveraged to copy the attribute based DynamoDb annotations onto the generated code):

```java
@Value
@Builder
@DynamoDbImmutable(builder = Customer.CustomerBuilder.class)
public static class Customer {
@Getter(onMethod = @__({@DynamoDbPartitionKey}))
private String accountId;

@Getter(onMethod = @__({@DynamoDbSortKey}))
private int subId;

@Getter(onMethod = @__({@DynamoDbSecondaryPartitionKey(indexNames = "customers_by_name")}))
private String name;

@Getter(onMethod = @__({@DynamoDbSecondarySortKey(indexNames = {"customers_by_date", "customers_by_name"})}))
private Instant createdDate;
}
```

### Non-blocking asynchronous operations
If your application requires non-blocking asynchronous calls to
DynamoDb, then you can use the asynchronous implementation of the
Expand All @@ -165,9 +255,10 @@ key differences:
of the library instead of the synchronous one (you will need to use
an asynchronous DynamoDb client from the SDK as well):
```java
DynamoDbEnhancedAsyncClient enhancedClient = DynamoDbEnhancedAsyncClient.builder()
.dynamoDbClient(dynamoDbAsyncClient)
.build();
DynamoDbEnhancedAsyncClient enhancedClient =
DynamoDbEnhancedAsyncClient.builder()
.dynamoDbClient(dynamoDbAsyncClient)
.build();
```

2. Operations that return a single data item will return a
Expand Down Expand Up @@ -424,7 +515,7 @@ public class Customer {
public String getName() { return this.name; }
public void setName(String name) { this.name = name;}

@DynamoDbFlatten(dynamoDbBeanClass = GenericRecord.class)
@DynamoDbFlatten
public GenericRecord getRecord() { return this.record; }
public void setRecord(GenericRecord record) { this.record = record;}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package software.amazon.awssdk.enhanced.dynamodb;

import java.util.Optional;
import software.amazon.awssdk.annotations.SdkPublicApi;

/**
* A metadata class that stores information about an index
*/
@SdkPublicApi
public interface IndexMetadata {
/**
* The name of the index
*/
String name();

/**
* The partition key for the index; if there is one.
*/
Optional<KeyAttributeMetadata> partitionKey();

/**
* The sort key for the index; if there is one.
*/
Optional<KeyAttributeMetadata> sortKey();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package software.amazon.awssdk.enhanced.dynamodb;

import software.amazon.awssdk.annotations.SdkPublicApi;

/**
* A metadata class that stores information about a key attribute
*/
@SdkPublicApi
public interface KeyAttributeMetadata {
/**
* The name of the key attribute
*/
String name();

/**
* The DynamoDB type of the key attribute
*/
AttributeValueType attributeValueType();
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package software.amazon.awssdk.enhanced.dynamodb;

import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType;
Expand Down Expand Up @@ -72,9 +73,36 @@ public interface TableMetadata {
* attribute when using the versioned record extension.
*
* @return A collection of all key attribute names for the table.
*
* @deprecated Use {@link #keyAttributes()} instead.
*/
@Deprecated
Collection<String> allKeys();

/**
* Returns metadata about all the known indices for this table.
* @return A collection of {@link IndexMetadata} containing information about the indices.
*/
Collection<IndexMetadata> indices();

/**
* Returns all custom metadata for this table. These entries are used by extensions to the library, therefore the
* value type of each metadata object stored in the map is not known and is provided as {@link Object}.
* <p>
* This method should not be used to inspect individual custom metadata objects, instead use
* {@link TableMetadata#customMetadataObject(String, Class)} ()} as that will perform a type-safety check on the
* retrieved object.
* @return A map of all the custom metadata for this table.
*/
Map<String, Object> customMetadata();

/**
* Returns metadata about all the known 'key' attributes for this table, such as primary and secondary index keys,
* or any other attribute that forms part of the structure of the table.
* @return A collection of {@link KeyAttributeMetadata} containing information about the keys.
*/
Collection<KeyAttributeMetadata> keyAttributes();

/**
* Returns the DynamoDb scalar attribute type associated with a key attribute if one is applicable.
* @param keyAttribute The key attribute name to return the scalar attribute type of.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,15 @@
package software.amazon.awssdk.enhanced.dynamodb;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema;
import software.amazon.awssdk.enhanced.dynamodb.mapper.ImmutableTableSchema;
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticImmutableTableSchema;
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;

/**
Expand All @@ -31,7 +36,6 @@
*/
@SdkPublicApi
public interface TableSchema<T> {

/**
* Returns a builder for the {@link StaticTableSchema} implementation of this interface which allows all attributes,
* tags and table structure to be directly declared in the builder.
Expand All @@ -43,6 +47,21 @@ static <T> StaticTableSchema.Builder<T> builder(Class<T> itemClass) {
return StaticTableSchema.builder(itemClass);
}

/**
* Returns a builder for the {@link StaticImmutableTableSchema} implementation of this interface which allows all
* attributes, tags and table structure to be directly declared in the builder.
* @param immutableItemClass The class of the immutable item this {@link TableSchema} will map records to.
* @param immutableBuilderClass The class that can be used to construct immutable items this {@link TableSchema}
* maps records to.
* @param <T> The type of the immutable item this {@link TableSchema} will map records to.
* @param <B> The type of the builder used by this {@link TableSchema} to construct immutable items with.
* @return A newly initialized {@link StaticImmutableTableSchema.Builder}
*/
static <T, B> StaticImmutableTableSchema.Builder<T, B> builder(Class<T> immutableItemClass,
Class<B> immutableBuilderClass) {
return StaticImmutableTableSchema.builder(immutableItemClass, immutableBuilderClass);
}

/**
* Scans a bean class that has been annotated with DynamoDb bean annotations and then returns a
* {@link BeanTableSchema} implementation of this interface that can map records to and from items of that bean
Expand All @@ -55,6 +74,44 @@ static <T> BeanTableSchema<T> fromBean(Class<T> beanClass) {
return BeanTableSchema.create(beanClass);
}

/**
* Scans an immutable class that has been annotated with DynamoDb immutable annotations and then returns a
* {@link ImmutableTableSchema} implementation of this interface that can map records to and from items of that
* immutable class.
*
* @param immutableClass The immutable class this {@link TableSchema} will map records to.
* @param <T> The type of the item this {@link TableSchema} will map records to.
* @return An initialized {@link ImmutableTableSchema}.
*/
static <T> ImmutableTableSchema<T> fromImmutableClass(Class<T> immutableClass) {
return ImmutableTableSchema.create(immutableClass);
}

/**
* Scans a class that has been annotated with DynamoDb enhanced client annotations and then returns an appropriate
* {@link TableSchema} implementation that can map records to and from items of that class. Currently supported
* top level annotations (see documentation on those classes for more information on how to use them):
* <p>
* {@link software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean}<br>
* {@link software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable}
*
* @param annotatedClass A class that has been annotated with DynamoDb enhanced client annotations.
* @param <T> The type of the item this {@link TableSchema} will map records to.
* @return An initialized {@link TableSchema}
*/
static <T> TableSchema<T> fromClass(Class<T> annotatedClass) {
if (annotatedClass.getAnnotation(DynamoDbImmutable.class) != null) {
return fromImmutableClass(annotatedClass);
}

if (annotatedClass.getAnnotation(DynamoDbBean.class) != null) {
return fromBean(annotatedClass);
}

throw new IllegalArgumentException("Class does not appear to be a valid DynamoDb annotated class. [class = " +
"\"" + annotatedClass + "\"]");
}

/**
* Takes a raw DynamoDb SDK representation of a record in a table and maps it to a Java object. A new object is
* created to fulfil this operation.
Expand Down Expand Up @@ -96,11 +153,11 @@ static <T> BeanTableSchema<T> fromBean(Class<T> beanClass) {
* Returns a single attribute value from the modelled object.
*
* @param item The modelled Java object to extract the attribute from.
* @param key The attribute name describing which attribute to extract.
* @param attributeName The attribute name describing which attribute to extract.
* @return A single {@link AttributeValue} representing the requested modelled attribute in the model object or
* null if the attribute has not been set with a value in the modelled object.
*/
AttributeValue attributeValue(T item, String key);
AttributeValue attributeValue(T item, String attributeName);

/**
* Returns the object that describes the structure of the table being modelled by the mapper. This includes
Expand All @@ -115,4 +172,19 @@ static <T> BeanTableSchema<T> fromBean(Class<T> beanClass) {
* @return The {@link EnhancedType} of the modelled item this TableSchema maps to.
*/
EnhancedType<T> itemType();

/**
* Returns a complete list of attribute names that are mapped by this {@link TableSchema}
*/
List<String> attributeNames();

/**
* A boolean value that represents whether this {@link TableSchema} is abstract which means that it cannot be used
* to directly create records as it is lacking required structural elements to map to a table, such as a primary
* key, but can be referred to and embedded by other schemata.
*
* @return true if it is abstract, and therefore cannot be used directly to create records but can be referred to
* by other schemata, and false if it is concrete and may be used to map records directly.
*/
boolean isAbstract();
}
Loading