Skip to content

Commit 08945b9

Browse files
committed
DynamoDb Enhanced Client: Add support for immutables with StaticImmutableTableSchema and ImmutableTableSchema
1 parent 47fd98c commit 08945b9

File tree

46 files changed

+6359
-717
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+6359
-717
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"type": "feature",
3+
"category": "DynamoDB Enhanced Client",
4+
"description": "Support for mapping to and from immutable Java objects using ImmutableTableSchema and StaticImmutableTableSchema."
5+
}

services-custom/dynamodb-enhanced/README.md

Lines changed: 98 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,10 @@ values used are also completely arbitrary.
4141
}
4242
```
4343

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

5050
If you would prefer to skip the slightly costly bean inference for a faster solution, you can instead declare your
@@ -155,6 +155,96 @@ index. Here's an example of how to do this:
155155
PageIterable<Customer> customersWithName =
156156
customersByName.query(r -> r.queryConditional(equalTo(k -> k.partitionValue("Smith"))));
157157
```
158+
159+
### Working with immutable data classes
160+
It is possible to have the DynamoDB Enhanced Client map directly to and from immutable data classes in Java. An
161+
immutable class is expected to only have getters and will also be associated with a separate builder class that
162+
is used to construct instances of the immutable data class. The DynamoDB annotation style for immutable classes is
163+
very similar to bean classes :
164+
165+
```java
166+
@DynamoDbImmutable(builder = Customer.Builder.class)
167+
public class Customer {
168+
private final String accountId;
169+
private final int subId;
170+
private final String name;
171+
private final Instant createdDate;
172+
173+
private Customer(Builder b) {
174+
this.accountId = b.accountId;
175+
this.subId = b.subId;
176+
this.name = b.name;
177+
this.createdDate = b.createdDate;
178+
}
179+
180+
// This method will be automatically discovered and used by the TableSchema
181+
public static Builder builder() { return new Builder(); }
182+
183+
@DynamoDbPartitionKey
184+
public String accountId() { return this.accountId; }
185+
186+
@DynamoDbSortKey
187+
public int subId() { return this.subId; }
188+
189+
@DynamoDbSecondaryPartitionKey(indexNames = "customers_by_name")
190+
public String name() { return this.name; }
191+
192+
@DynamoDbSecondarySortKey(indexNames = {"customers_by_date", "customers_by_name"})
193+
public Instant createdDate() { return this.createdDate; }
194+
195+
public static final class Builder {
196+
private String accountId;
197+
private int subId;
198+
private String name;
199+
private Instant createdDate;
200+
201+
private Builder() {}
202+
203+
public Builder accountId(String accountId) { this.accountId = accountId; return this; }
204+
public Builder subId(int subId) { this.subId = subId; return this; }
205+
public Builder name(String name) { this.name = name; return this; }
206+
public Builder createdDate(Instant createdDate) { this.createdDate = createdDate; return this; }
207+
208+
// This method will be automatically discovered and used by the TableSchema
209+
public Customer build() { return new Customer(this); }
210+
}
211+
}
212+
```
213+
214+
The following requirements must be met for a class annotated with @DynamoDbImmutable:
215+
1. Every method on the immutable class that is not an override of Object.class or annotated with @DynamoDbIgnore must
216+
be a getter for an attribute of the database record.
217+
1. Every getter in the immutable class must have a corresponding setter on the builder class that has a case-sensitive
218+
matching name.
219+
1. EITHER: the builder class must have a public default constructor; OR: there must be a public static method named
220+
'builder' on the immutable class that takes no parameters and returns an instance of the builder class.
221+
1. The builder class must have a public method named 'build' that takes no parameters and returns an instance of the
222+
immutable class.
223+
224+
There are third-party library that help generate a lot of the boilerplate code associated with immutable objects.
225+
The DynamoDb Enhanced client should work with these libraries as long as they follow the conventions detailed
226+
in this section. Here's an example of the immutable Customer class using Lombok with DynamoDb annotations (note
227+
how Lombok's 'onMethod' feature is leveraged to copy the attribute based DynamoDb annotations onto the generated code):
228+
229+
```java
230+
@Value
231+
@Builder
232+
@DynamoDbImmutable(builder = Customer.CustomerBuilder.class)
233+
public static class Customer {
234+
@Getter(onMethod = @__({@DynamoDbPartitionKey}))
235+
private String accountId;
236+
237+
@Getter(onMethod = @__({@DynamoDbSortKey}))
238+
private int subId;
239+
240+
@Getter(onMethod = @__({@DynamoDbSecondaryPartitionKey(indexNames = "customers_by_name")}))
241+
private String name;
242+
243+
@Getter(onMethod = @__({@DynamoDbSecondarySortKey(indexNames = {"customers_by_date", "customers_by_name"})}))
244+
private Instant createdDate;
245+
}
246+
```
247+
158248
### Non-blocking asynchronous operations
159249
If your application requires non-blocking asynchronous calls to
160250
DynamoDb, then you can use the asynchronous implementation of the
@@ -165,9 +255,10 @@ key differences:
165255
of the library instead of the synchronous one (you will need to use
166256
an asynchronous DynamoDb client from the SDK as well):
167257
```java
168-
DynamoDbEnhancedAsyncClient enhancedClient = DynamoDbEnhancedAsyncClient.builder()
169-
.dynamoDbClient(dynamoDbAsyncClient)
170-
.build();
258+
DynamoDbEnhancedAsyncClient enhancedClient =
259+
DynamoDbEnhancedAsyncClient.builder()
260+
.dynamoDbClient(dynamoDbAsyncClient)
261+
.build();
171262
```
172263
173264
2. Operations that return a single data item will return a
@@ -424,7 +515,7 @@ public class Customer {
424515
public String getName() { return this.name; }
425516
public void setName(String name) { this.name = name;}
426517

427-
@DynamoDbFlatten(dynamoDbBeanClass = GenericRecord.class)
518+
@DynamoDbFlatten
428519
public GenericRecord getRecord() { return this.record; }
429520
public void setRecord(GenericRecord record) { this.record = record;}
430521
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.enhanced.dynamodb;
17+
18+
import java.util.Optional;
19+
import software.amazon.awssdk.annotations.SdkPublicApi;
20+
21+
/**
22+
* A metadata class that stores information about an index
23+
*/
24+
@SdkPublicApi
25+
public interface IndexMetadata {
26+
/**
27+
* The name of the index
28+
*/
29+
String name();
30+
31+
/**
32+
* The partition key for the index; if there is one.
33+
*/
34+
Optional<KeyAttributeMetadata> partitionKey();
35+
36+
/**
37+
* The sort key for the index; if there is one.
38+
*/
39+
Optional<KeyAttributeMetadata> sortKey();
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.enhanced.dynamodb;
17+
18+
import software.amazon.awssdk.annotations.SdkPublicApi;
19+
20+
/**
21+
* A metadata class that stores information about a key attribute
22+
*/
23+
@SdkPublicApi
24+
public interface KeyAttributeMetadata {
25+
/**
26+
* The name of the key attribute
27+
*/
28+
String name();
29+
30+
/**
31+
* The DynamoDB type of the key attribute
32+
*/
33+
AttributeValueType attributeValueType();
34+
}

services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/TableMetadata.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package software.amazon.awssdk.enhanced.dynamodb;
1717

1818
import java.util.Collection;
19+
import java.util.Map;
1920
import java.util.Optional;
2021
import software.amazon.awssdk.annotations.SdkPublicApi;
2122
import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType;
@@ -72,9 +73,36 @@ public interface TableMetadata {
7273
* attribute when using the versioned record extension.
7374
*
7475
* @return A collection of all key attribute names for the table.
76+
*
77+
* @deprecated Use {@link #keyAttributes()} instead.
7578
*/
79+
@Deprecated
7680
Collection<String> allKeys();
7781

82+
/**
83+
* Returns metadata about all the known indices for this table.
84+
* @return A collection of {@link IndexMetadata} containing information about the indices.
85+
*/
86+
Collection<IndexMetadata> indices();
87+
88+
/**
89+
* Returns all custom metadata for this table. These entries are used by extensions to the library, therefore the
90+
* value type of each metadata object stored in the map is not known and is provided as {@link Object}.
91+
* <p>
92+
* This method should not be used to inspect individual custom metadata objects, instead use
93+
* {@link TableMetadata#customMetadataObject(String, Class)} ()} as that will perform a type-safety check on the
94+
* retrieved object.
95+
* @return A map of all the custom metadata for this table.
96+
*/
97+
Map<String, Object> customMetadata();
98+
99+
/**
100+
* Returns metadata about all the known 'key' attributes for this table, such as primary and secondary index keys,
101+
* or any other attribute that forms part of the structure of the table.
102+
* @return A collection of {@link KeyAttributeMetadata} containing information about the keys.
103+
*/
104+
Collection<KeyAttributeMetadata> keyAttributes();
105+
78106
/**
79107
* Returns the DynamoDb scalar attribute type associated with a key attribute if one is applicable.
80108
* @param keyAttribute The key attribute name to return the scalar attribute type of.

services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/TableSchema.java

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,15 @@
1616
package software.amazon.awssdk.enhanced.dynamodb;
1717

1818
import java.util.Collection;
19+
import java.util.List;
1920
import java.util.Map;
2021
import software.amazon.awssdk.annotations.SdkPublicApi;
2122
import software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema;
23+
import software.amazon.awssdk.enhanced.dynamodb.mapper.ImmutableTableSchema;
24+
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticImmutableTableSchema;
2225
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema;
26+
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;
27+
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable;
2328
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
2429

2530
/**
@@ -31,7 +36,6 @@
3136
*/
3237
@SdkPublicApi
3338
public interface TableSchema<T> {
34-
3539
/**
3640
* Returns a builder for the {@link StaticTableSchema} implementation of this interface which allows all attributes,
3741
* tags and table structure to be directly declared in the builder.
@@ -43,6 +47,21 @@ static <T> StaticTableSchema.Builder<T> builder(Class<T> itemClass) {
4347
return StaticTableSchema.builder(itemClass);
4448
}
4549

50+
/**
51+
* Returns a builder for the {@link StaticImmutableTableSchema} implementation of this interface which allows all
52+
* attributes, tags and table structure to be directly declared in the builder.
53+
* @param immutableItemClass The class of the immutable item this {@link TableSchema} will map records to.
54+
* @param immutableBuilderClass The class that can be used to construct immutable items this {@link TableSchema}
55+
* maps records to.
56+
* @param <T> The type of the immutable item this {@link TableSchema} will map records to.
57+
* @param <B> The type of the builder used by this {@link TableSchema} to construct immutable items with.
58+
* @return A newly initialized {@link StaticImmutableTableSchema.Builder}
59+
*/
60+
static <T, B> StaticImmutableTableSchema.Builder<T, B> builder(Class<T> immutableItemClass,
61+
Class<B> immutableBuilderClass) {
62+
return StaticImmutableTableSchema.builder(immutableItemClass, immutableBuilderClass);
63+
}
64+
4665
/**
4766
* Scans a bean class that has been annotated with DynamoDb bean annotations and then returns a
4867
* {@link BeanTableSchema} implementation of this interface that can map records to and from items of that bean
@@ -55,6 +74,44 @@ static <T> BeanTableSchema<T> fromBean(Class<T> beanClass) {
5574
return BeanTableSchema.create(beanClass);
5675
}
5776

77+
/**
78+
* Scans an immutable class that has been annotated with DynamoDb immutable annotations and then returns a
79+
* {@link ImmutableTableSchema} implementation of this interface that can map records to and from items of that
80+
* immutable class.
81+
*
82+
* @param immutableClass The immutable class this {@link TableSchema} will map records to.
83+
* @param <T> The type of the item this {@link TableSchema} will map records to.
84+
* @return An initialized {@link ImmutableTableSchema}.
85+
*/
86+
static <T> ImmutableTableSchema<T> fromImmutableClass(Class<T> immutableClass) {
87+
return ImmutableTableSchema.create(immutableClass);
88+
}
89+
90+
/**
91+
* Scans a class that has been annotated with DynamoDb enhanced client annotations and then returns an appropriate
92+
* {@link TableSchema} implementation that can map records to and from items of that class. Currently supported
93+
* top level annotations (see documentation on those classes for more information on how to use them):
94+
* <p>
95+
* {@link software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean}<br>
96+
* {@link software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable}
97+
*
98+
* @param annotatedClass A class that has been annotated with DynamoDb enhanced client annotations.
99+
* @param <T> The type of the item this {@link TableSchema} will map records to.
100+
* @return An initialized {@link TableSchema}
101+
*/
102+
static <T> TableSchema<T> fromClass(Class<T> annotatedClass) {
103+
if (annotatedClass.getAnnotation(DynamoDbImmutable.class) != null) {
104+
return fromImmutableClass(annotatedClass);
105+
}
106+
107+
if (annotatedClass.getAnnotation(DynamoDbBean.class) != null) {
108+
return fromBean(annotatedClass);
109+
}
110+
111+
throw new IllegalArgumentException("Class does not appear to be a valid DynamoDb annotated class. [class = " +
112+
"\"" + annotatedClass + "\"]");
113+
}
114+
58115
/**
59116
* Takes a raw DynamoDb SDK representation of a record in a table and maps it to a Java object. A new object is
60117
* created to fulfil this operation.
@@ -96,11 +153,11 @@ static <T> BeanTableSchema<T> fromBean(Class<T> beanClass) {
96153
* Returns a single attribute value from the modelled object.
97154
*
98155
* @param item The modelled Java object to extract the attribute from.
99-
* @param key The attribute name describing which attribute to extract.
156+
* @param attributeName The attribute name describing which attribute to extract.
100157
* @return A single {@link AttributeValue} representing the requested modelled attribute in the model object or
101158
* null if the attribute has not been set with a value in the modelled object.
102159
*/
103-
AttributeValue attributeValue(T item, String key);
160+
AttributeValue attributeValue(T item, String attributeName);
104161

105162
/**
106163
* Returns the object that describes the structure of the table being modelled by the mapper. This includes
@@ -115,4 +172,19 @@ static <T> BeanTableSchema<T> fromBean(Class<T> beanClass) {
115172
* @return The {@link EnhancedType} of the modelled item this TableSchema maps to.
116173
*/
117174
EnhancedType<T> itemType();
175+
176+
/**
177+
* Returns a complete list of attribute names that are mapped by this {@link TableSchema}
178+
*/
179+
List<String> attributeNames();
180+
181+
/**
182+
* A boolean value that represents whether this {@link TableSchema} is abstract which means that it cannot be used
183+
* to directly create records as it is lacking required structural elements to map to a table, such as a primary
184+
* key, but can be referred to and embedded by other schemata.
185+
*
186+
* @return true if it is abstract, and therefore cannot be used directly to create records but can be referred to
187+
* by other schemata, and false if it is concrete and may be used to map records directly.
188+
*/
189+
boolean isAbstract();
118190
}

0 commit comments

Comments
 (0)