-
Notifications
You must be signed in to change notification settings - Fork 877
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
DynamoDB-enhanced: Add support for polymorphic subtypes to mapper #2861
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
{ | ||
"category": "DynamoDB Enhanced Client", | ||
"contributor": "bmaizels", | ||
"type": "feature", | ||
"description": "Added support for polymorphic mapping of subtypes to better support single-table design." | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
## Overview | ||
|
||
Mid-level DynamoDB mapper/abstraction for Java using the v2 AWS SDK. | ||
A library that enhances DynamoDB operations by directly mapping your Java data objects to and from records in your | ||
DynamoDB tables. | ||
|
||
## Getting Started | ||
All the examples below use a fictional Customer class. This class is | ||
|
@@ -253,6 +254,85 @@ how Lombok's 'onMethod' feature is leveraged to copy the attribute based DynamoD | |
} | ||
``` | ||
|
||
### Using subtypes to assist with single-table design | ||
It's considered a best practice in some situations to combine entities of various types into a single table in DynamoDb | ||
to enable the querying of multiple related entities without the need to actually join data across multiple tables. The | ||
enhanced client assists with this by supporting polymorphic mapping into distinct subtypes. | ||
|
||
Let's say you have a customer: | ||
|
||
```java | ||
public class Customer { | ||
String getCustomerId(); | ||
void setId(String id); | ||
|
||
String getName(); | ||
void setName(String name); | ||
} | ||
``` | ||
|
||
And an order that's associated with a customer: | ||
|
||
```java | ||
public class Order { | ||
String getOrderId(); | ||
void setOrderId(); | ||
|
||
String getCustomerId(); | ||
void setCustomerId(); | ||
} | ||
``` | ||
|
||
You could choose to store both of these in a single table that is indexed by customer ID, and create a TableSchema that | ||
is capable of mapping both types of entities into a common supertype: | ||
|
||
```java | ||
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; | ||
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; | ||
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypeName; | ||
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypes; | ||
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypes.Subtype; | ||
|
||
@DynamoDbBean | ||
@DynamoDbSubtypes({ | ||
@Subtype(name = "CUSTOMER", subtypeClass = Customer.class), | ||
@Subtype(name = "ORDER", subtypeClass = Order.class)}) | ||
public class CustomerRelatedEntity { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can be abstract and/or the annotation enforce that ( I see its abstract in code below) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Java code would be way neater if this could be an interface and classes implementing it. Is that a feasible option? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @dvlato could you elaborate on exactly what you are proposing? Maybe with an example? |
||
@DynamoDbSubtypeName | ||
String getEntityType(); | ||
void setEntityType(); | ||
|
||
@DynamoDbPartitionKey | ||
String getCustomerId(); | ||
void setCustomerId(); | ||
} | ||
|
||
@DynamoDbBean | ||
public class Customer extends CustomerRelatedEntity { | ||
String getName(); | ||
void setName(String name); | ||
} | ||
|
||
@DynamoDbBean | ||
public class Order extends CustomerRelatedEntity { | ||
String getOrderId(); | ||
void setOrderId(); | ||
} | ||
``` | ||
|
||
Now all you have to do is create a TableSchema that maps the supertype class: | ||
```java | ||
TableSchema<CustomerRelatedEntity> tableSchema = TableSchema.fromClass(CustomerRelatedEntity.class); | ||
``` | ||
Now you have a `TableSchema` that can map any objects of both `Customer` and `Order` and write them to the table, | ||
and can also read any record from the table and correctly instantiate it using the subtype class. So it's now possible | ||
to write a single query that will return both the customer record and all order records associated with a specific | ||
customer ID. | ||
|
||
As with all the other `TableSchema` implementations, a static version is provided that allows reflective introspection | ||
to be skipped entirely and is recommended for applications where cold-start latency is critical. See the javadocs for | ||
`StaticPolymorphicTableSchema` for an example of how to use this. | ||
|
||
### Non-blocking asynchronous operations | ||
If your application requires non-blocking asynchronous calls to | ||
DynamoDb, then you can use the asynchronous implementation of the | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
/* | ||
* 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.internal.mapper; | ||
|
||
import java.util.Optional; | ||
import java.util.function.Consumer; | ||
import software.amazon.awssdk.annotations.SdkInternalApi; | ||
import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; | ||
import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; | ||
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTag; | ||
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableMetadata; | ||
|
||
@SdkInternalApi | ||
public class SubtypeNameTag implements StaticAttributeTag { | ||
private static final SubtypeNameTag INSTANCE = new SubtypeNameTag(); | ||
private static final String CUSTOM_METADATA_KEY = "SubtypeName"; | ||
|
||
private SubtypeNameTag() { | ||
} | ||
|
||
public static Optional<String> resolve(TableMetadata tableMetadata) { | ||
return tableMetadata.customMetadataObject(CUSTOM_METADATA_KEY, String.class); | ||
} | ||
|
||
@Override | ||
public Consumer<StaticTableMetadata.Builder> modifyMetadata(String attributeName, | ||
AttributeValueType attributeValueType) { | ||
if (!AttributeValueType.S.equals(attributeValueType)) { | ||
throw new IllegalArgumentException( | ||
String.format("Attribute '%s' of type %s is not a suitable type to be used as a subtype name. Only string is " | ||
+ "supported for this purpose.", attributeName, attributeValueType.name())); | ||
} | ||
|
||
return metadata -> | ||
metadata.addCustomMetadataObject(CUSTOM_METADATA_KEY, attributeName); | ||
} | ||
|
||
public static SubtypeNameTag create() { | ||
return INSTANCE; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be useful to highlight here a sample multi-schema table for matching the proposed annotations