Use DynamoDBMapper to Work with Amazon DynamoDB from Java

Articles & Tutorials>Use DynamoDBMapper to Work with Amazon DynamoDB from Java
Tips on using the DynamoDBMapper utility, part of the AWS SDK for Java, to model objects in Amazon DynamoDB.

Details

Submitted By: Zach Musgrave
AWS Products Used: AWS SDK for Java, Amazon DynamoDB
Language(s): English
Created On: October 2, 2012 9:36 PM GMT
Last Updated: August 21, 2013 5:42 PM GMT

Use DynamoDBMapper to Work with Amazon DynamoDB from Java

DynamoDBMapper, a new feature of the AWS SDK for Java, makes it easier to access Amazon DynamoDB tables from Java. Sometimes, working with databases from an object-oriented language like Java can be a frustrating experience. You often want to represent the rows in your database tables as instances of your domain classes, but you don't want to write a bunch of boilerplate code to transform database results into objects and vice versa. For traditional database servers such as MySQL and Oracle, you can use tools such as Hibernate to automate this aspect of development, but for newer NoSQL databases, you often have to either roll your own framework or rely on the community to provide one. DynamoDBMapper solves this problem for Amazon DynamoDB.

The Problem: Re-inventing the wheel

A typical Amazon DynamoDB response looks something like this:

{
  "Item":
    {"friends":{"SS":["Lynda, Aaron"]},
     "status":{"S":"online"},
     "id":{"N":"123456"}
    },
  "ConsumedCapacityUnits": 1
 }

Let's say that you want to map these attributes to a domain class, so you write a simple Plain Old Java Object (POJO).

public class User {
    
    private Integer id;
    private Set<String> friends;
    private String status;
    
    public Integer getId() { return id; }    
    public void setId(Integer id) { this.id = id; }
    
    public Set<String> getFriends() { return friends; }    
    public void setFriends(Set<String> friends) { this.friends = friends; }
    
    public String getStatus() { return status; }        
    public void setStatus(String status) { this.status = status; }    
}
	

Then, to get instances of this class into and out of the database, you have to write the following code:

public void save(AmazonDynamoDB dynamo) {
    Map item = new HashMap();
    item.put("id", new AttributeValue().withN(String.valueOf(getId())));
    if ( getFriends() != null && !getFriends().isEmpty() )
        item.put("friends", new AttributeValue().withSS(getFriends()));
    if ( getStatus() != null && getStatus().length() > 0 )
        item.put("status", new AttributeValue().withS(getStatus()));
    dynamo.putItem(new PutItemRequest().withTableName("users").withItem(item));
}

public static User load(AmazonDynamoDB dynamo, Integer id) {
    GetItemResult result = dynamo.getItem(new GetItemRequest().withTableName("mytable").withKey(
            new Key().withHashKeyElement(new AttributeValue().withN(String.valueOf(id)))));
    if ( result.getItem() == null )
        return null;

    User user = new User();
    user.setId(id);
    if ( result.getItem().get("status") != null )
        user.setStatus(result.getItem().get("status").getS());
    if ( result.getItem().get("friends") != null )
        user.setFriends(new HashSet(result.getItem().get("friends").getSS()));

    return user;
}

This process is time-consuming and error prone, especially as the number of domain classes grows. You shouldn't have to write this kind of code to interact with a database.

The Solution: Introducing DynamoDBMapper

The AWS SDK for Java provides DynamoDBMapper, a high-level interface that automates the process of getting your objects into Amazon DynamoDB and back out again. To use DynamoDBMapper, annotate your class to tell Amazon DynamoDB how your class properties map to Amazon DynamoDB attributes. For the example above, this would look like:

@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; }
}

Now you can get your objects into and out of Amazon DynamoDB with just a few lines of code:

User newUser = new User();
newUser.setId(100);
newUser.setFriends(new HashSet(Arrays.asList("Frank", "Jeremy", "Linda")));
newUser.setStatus("pending");
AmazonDynamoDB dynamo = new AmazonDynamoDBClient(awsCredentials);
DynamoDBMapper mapper = new DynamoDBMapper(dynamo);

// save a new item
mapper.save(newUser);

...
 
// update the existing item
newUser.setStatus("active");
newUser.getFriends().remove("Jeremy");
mapper.save(newUser);

...

// delete the item
User user = mapper.load(User.class, new Integer(100));
mapper.delete(user);

Advanced Features

DynamoDBMapper supports a number of features to make it easier to work with Amazon DynamoDB.

Auto-generated Keys

If we modify our previous example to change the key property to a String, we can tell DynamoDBMapper to automatically generate a UUID on our behalf when we save a new item to Amazon DynamoDB.

@DynamoDBTable(tableName = "users")
public class User {
    
    private String id;
   
    ...
        
    @DynamoDBHashKey
    @DynamoDBAutoGeneratedKey
    public String getId() { return id; }    
    public void setId(String id) { this.id = id; }
    
    ...
}

User newUser = new User();
newUser.setFriends(new HashSet(Arrays.asList("Frank", "Jeremy", "Linda")));
newUser.setStatus("pending");
mapper.save(newUser);
System.out.println(newUser.getId());  // 849e8ab0-5438-11e1-b86c-0800200c9a66

Optimistic Locking

For highly concurrent systems, DynamoDBMapper helps you implement optimistic locking using Amazon DynamoDB's updateItem API with an Expected clause. This allows you to detect when another process has modified an item you're working on and allows you to retry the transaction with the updated data.

@DynamoDBTable(tableName = "users")
public class User {
    
    private Integer version;
   
    ...
        
    @DynamoDBVersionAttribute
    public Integer getVersion() { return version; }    
    public void setVersion(Integer version) { this.version = version; }
    
    ...
}

User user = mapper.load(User.class, userId);
System.out.println(user.getVersion()); // 8
user.setStatus("pending");
mapper.save(user);
System.out.println(user.getVersion()); // 9

...

public void addFriend(String userId, String friend) {
    User user = mapper.load(User.class, userId);
    user.getFriends().add(friend);
    try {
        mapper.save(user);
    } catch (ConditionalCheckFailedException e) {
        // Another process updated this item after we loaded it, so try again with the newest data
        addFriend(userId, friend);
    }
}

Custom Marshallers

Out of the box, DynamoDBMapper only knows how to store numeric, byte array, and string types. However, you can easily write an adapter to store any type that can be represented as a String. For example, if you want to store a POJO inside your POJO, DynamoDBMapper provides a generic JSON marshaller that stores nested objects as their basic JSON representations.

public class Profile {
    Date dateOfBirth;
    String favoriteColor;
    
    public Date getDateOfBirth() { return dateOfBirth; }    
    public void setDateOfBirth(Date dateOfBirth) { this.dateOfBirth = dateOfBirth; }
    
    public String getFavoriteColor() { return favoriteColor; }    
    public void setFavoriteColor(String favoriteColor) { this.favoriteColor = favoriteColor; }        
}

public class ProfileMarshaller extends JsonMarshaller<Profile> { }

@DynamoDBTable(tableName = "users")
public class User {
    
    private Profile profile;
   
    ...
        
    @DynamoDBAttribute
    @DynamoDBMarshalling(marshallerClass = ProfileMarshaller.class)
    public Profile getProfile() { return profile; }
    public void setProfile(Profile profile) { this.profile = profile; }
    
    ...
}

More Resources

To learn more about DynamoDBMapper, read Using the Object Persistence Model with Amazon DynamoDB. You can also visit the AWS forums to discuss DynamoDBMapper with other developers and get answers to your questions.

©2014, Amazon Web Services, Inc. or its affiliates. All rights reserved.