AWS Developer Tools Blog
Using Custom Marshallers to Store Complex Objects in Amazon DynamoDB
Over the past few months, we’ve talked about using the AWS SDK for Java to store and retrieve Java objects in Amazon DynamoDB. Our first post was about the basic features of the DynamoDBMapper
framework, and then we zeroed in on the behavior of auto-paginated scan. Today we’re going to spend some time talking about how to store complex types in DynamoDB. We’ll be working with the User
class again, reproduced here:
@DynamoDBTable(tableName = "users")
public class User {
private Integer id;
private Set<String> friends;
private String status;
@DynamoDBHashKey
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
@DynamoDBAttribute
public Set<String> getFriends() { return friends; }
public void setFriends(Set<String> friends) { this.friends = friends; }
@DynamoDBAttribute
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
@DynamoDBAttribute
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
}
Out of the box, DynamoDBMapper
works with String
, Date
, and any numeric type such as int
, Integer
, byte
, Long
, etc. But what do you do when your domain object contains a reference to a complex type that you want persisted into DynamoDB?
Let’s imagine that we want to store the phone number for each User
in the system, and that we’re working with a PhoneNumber
class to represent it. For the sake of brevity, we are assuming it’s an American phone number. Our simple PhoneNumber
POJO looks like this:
public class PhoneNumber {
private String areaCode;
private String exchange;
private String subscriberLineIdentifier;
public String getAreaCode() { return areaCode; }
public void setAreaCode(String areaCode) { this.areaCode = areaCode; }
public String getExchange() { return exchange; }
public void setExchange(String exchange) { this.exchange = exchange; }
public String getSubscriberLineIdentifier() { return subscriberLineIdentifier; }
public void setSubscriberLineIdentifier(String subscriberLineIdentifier) { this.subscriberLineIdentifier = subscriberLineIdentifier; }
}
If we try to store a reference to this class in our User
class, DynamoDBMapper
will complain because it doesn’t know how to represent the PhoneNumber
class as one of DynamoDB’s basic data types.
Introducing the @DynamoDBMarshalling annotation
The DynamoDBMapper
framework supports this use case by allowing you to specify how to convert your class into a String
and vice versa. All you have to do is implement the DynamoDBMarshaller
interface for your domain object. For a phone number, we can represent it using the standard (xxx) xxx-xxxx pattern with the following class:
public class PhoneNumberMarshaller implements DynamoDBMarshaller<PhoneNumber>
{
@Override
public String marshall(PhoneNumber number) {
return "(" + number.getAreaCode() + ") " + number.getExchange() + "-" + number.getSubscriberLineIdentifier();
}
@Override
public PhoneNumber unmarshall(Class<PhoneNumber> clazz, String s) {
String[] areaCodeAndNumber = s.split(" ");
String areaCode = areaCodeAndNumber[0].substring(1,4);
String[] exchangeAndSlid = areaCodeAndNumber[1].split("-");
PhoneNumber number = new PhoneNumber();
number.setAreaCode(areaCode);
number.setExchange(exchangeAndSlid[0]);
number.setSubscriberLineIdentifier(exchangeAndSlid[1]);
return number;
}
}
Note that the DynamoDBMarshaller
interface is templatized on the domain object you’re working with, making this interface strictly typed.
Now that we have a class that knows how to convert our PhoneNumber
class into a String
and back, we just need to tell the DynamoDBMapper
framework about it. We do so with the @DynamoDBMarshalling
annotation.
@DynamoDBTable(tableName = "users")
public class User {
...
@DynamoDBMarshalling (marshallerClass = PhoneNumberMarshaller.class)
public PhoneNumber getPhoneNumber() { return phoneNumber; }
public void setPhoneNumber(PhoneNumber phoneNumber) { this.phoneNumber = phoneNumber; }
}
Built-in support for JSON representation
The above example uses a very compact String
representation of a phone number to use as little space in your DynamoDB table as possible. But if you’re not overly concerned about storage costs or space usage, you can just use the built-in JSON marshaling capability to marshal your domain object. Defining a JSON marshaller class takes just a single line of code:
class PhoneNumberJSONMarshaller extends JsonMarshaller<PhoneNumber> { }
However, the trade-off of using this built-in marshaller is that it produces a String
representation that’s more verbose than you could write yourself. A phone number marshaled with this class would end up looking like this (with spaces added for clarity):
{ "areaCode" : "xxx", "exchange: : "xxx", "subscriberLineIdentifier" : "xxxx" }
When writing a custom marshaller, you’ll also want to consider how easy it will be to write a scan filter that can find a particular value. Our compact phone number representation will be much easier to scan for than the JSON representation.
We’re always looking for ways to make our customers’ lives easier, so please let us know how you’re using DynamoDBMapper
to store complex objects, and what marshaling patterns have worked well for you. Share your success stories or complaints in the comments!