All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.amazonaws.services.dynamodb.datamodeling.DynamoDBMapper Maven / Gradle / Ivy

Go to download

The Amazon Web Services SDK for Java provides Java APIs for building software on AWS' cost-effective, scalable, and reliable infrastructure products. The AWS Java SDK allows developers to code against APIs for all of Amazon's infrastructure web services (Amazon S3, Amazon EC2, Amazon SQS, Amazon Relational Database Service, Amazon AutoScaling, etc).

The newest version!
/*
 * Copyright 2011-2014 Amazon Technologies, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at:
 *
 *    http://aws.amazon.com/apache2.0
 *
 * 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 com.amazonaws.services.dynamodb.datamodeling;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import com.amazonaws.AmazonClientException;
import com.amazonaws.AmazonWebServiceRequest;
import com.amazonaws.services.dynamodb.AmazonDynamoDB;
import com.amazonaws.services.dynamodb.datamodeling.DynamoDBMapperConfig.ConsistentReads;
import com.amazonaws.services.dynamodb.datamodeling.DynamoDBMapperConfig.SaveBehavior;
import com.amazonaws.services.dynamodb.model.AttributeValue;
import com.amazonaws.services.dynamodb.model.AttributeValueUpdate;
import com.amazonaws.services.dynamodb.model.BatchGetItemRequest;
import com.amazonaws.services.dynamodb.model.BatchGetItemResult;
import com.amazonaws.services.dynamodb.model.BatchResponse;
import com.amazonaws.services.dynamodb.model.BatchWriteItemRequest;
import com.amazonaws.services.dynamodb.model.BatchWriteItemResult;
import com.amazonaws.services.dynamodb.model.ConditionalCheckFailedException;
import com.amazonaws.services.dynamodb.model.DeleteItemRequest;
import com.amazonaws.services.dynamodb.model.DeleteRequest;
import com.amazonaws.services.dynamodb.model.ExpectedAttributeValue;
import com.amazonaws.services.dynamodb.model.GetItemRequest;
import com.amazonaws.services.dynamodb.model.GetItemResult;
import com.amazonaws.services.dynamodb.model.Key;
import com.amazonaws.services.dynamodb.model.KeysAndAttributes;
import com.amazonaws.services.dynamodb.model.PutItemRequest;
import com.amazonaws.services.dynamodb.model.PutRequest;
import com.amazonaws.services.dynamodb.model.QueryRequest;
import com.amazonaws.services.dynamodb.model.QueryResult;
import com.amazonaws.services.dynamodb.model.ScanRequest;
import com.amazonaws.services.dynamodb.model.ScanResult;
import com.amazonaws.services.dynamodb.model.UpdateItemRequest;
import com.amazonaws.services.dynamodb.model.WriteRequest;
import com.amazonaws.util.VersionInfoUtils;

/**
 * Object mapper for domain-object interaction with DynamoDB.
 * 

* To use, annotate domain classes with the annotations found in the * com.amazonaws.services.dynamodb.datamodeling package. A minimal example: * *

 * @DynamoDBTable(tableName = "TestTable")
 * public class TestClass {
 *
 *     private Long key;
 *     private double rangeKey;
 *     private Long version;
 *
 *     private Set<Integer> integerSetAttribute;
 *
 *     @DynamoDBHashKey
 *     public Long getKey() {
 *         return key;
 *     }
 *
 *     public void setKey(Long key) {
 *         this.key = key;
 *     }
 *
 *     @DynamoDBRangeKey
 *     public double getRangeKey() {
 *         return rangeKey;
 *     }
 *
 *     public void setRangeKey(double rangeKey) {
 *         this.rangeKey = rangeKey;
 *     }
 *
 *     @DynamoDBAttribute(attributeName = "integerSetAttribute")
 *     public Set<Integer> getIntegerAttribute() {
 *         return integerSetAttribute;
 *     }
 *
 *     public void setIntegerAttribute(Set<Integer> integerAttribute) {
 *         this.integerSetAttribute = integerAttribute;
 *     }
 *
 *     @DynamoDBVersionAttribute
 *     public Long getVersion() {
 *         return version;
 *     }
 *
 *     public void setVersion(Long version) {
 *         this.version = version;
 *     }
 * }
 * 
*

* Save instances of annotated classes to DynamoDB, retrieve them, and delete * them using the {@link DynamoDBMapper} class, as in the following example. * *

 * DynamoDBMapper mapper = new DynamoDBMapper(dynamoDBClient);
 * Long hashKey = 105L;
 * double rangeKey = 1.0d;
 * TestClass obj = mapper.load(TestClass.class, hashKey, rangeKey);
 * obj.getIntegerAttribute().add(42);
 * mapper.save(obj);
 * mapper.delete(obj);
 * 
*

* When using the save, load, and delete methods, {@link DynamoDBMapper} will * throw {@link DynamoDBMappingException}s to indicate that domain classes are * incorrectly annotated or otherwise incompatible with this class. Service * exceptions will always be propagated as {@link AmazonClientException}, and * DynamoDB-specific subclasses such as {@link ConditionalCheckFailedException} * will be used when possible. *

* This class is thread-safe and can be shared between threads. It's also very * lightweight, so it doesn't need to be. * * @see DynamoDBTable * @see DynamoDBHashKey * @see DynamoDBRangeKey * @see DynamoDBAutoGeneratedKey * @see DynamoDBAttribute * @see DynamoDBVersionAttribute * @see DynamoDBIgnore * @see DynamoDBMarshalling * @see DynamoDBMapperConfig * * @deprecated Use {@link com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper} instead. */ @Deprecated public class DynamoDBMapper { private final AmazonDynamoDB db; private final DynamoDBMapperConfig config; private static final DynamoDBReflector reflector = new DynamoDBReflector(); /** * User agent for requests made using the {@link DynamoDBMapper}. */ private static final String USER_AGENT = DynamoDBMapper.class.getName() + "/" + VersionInfoUtils.getVersion(); /** * Constructs a new mapper with the service object given, using the default * configuration. * * @param dynamoDB * The service object to use for all service calls. * @see DynamoDBMapperConfig#DEFAULT */ public DynamoDBMapper(AmazonDynamoDB dynamoDB) { this(dynamoDB, DynamoDBMapperConfig.DEFAULT); } /** * Constructs a new mapper with the service object and configuration given. * * @param dynamoDB * The service object to use for all service calls. * @param config * The default configuration to use for all service calls. It can * be overridden on a per-operation basis. */ public DynamoDBMapper(AmazonDynamoDB dynamoDB, DynamoDBMapperConfig config) { this.db = dynamoDB; this.config = config; } /** * Loads an object with the hash key given and a configuration override. * This configuration overrides the default provided at object construction. * * @see DynamoDBMapper#load(Class, Object, Object, DynamoDBMapperConfig) */ public T load(Class clazz, Object hashKey, DynamoDBMapperConfig config) { return load(clazz, hashKey, null, config); } /** * Loads an object with the hash key given, using the default configuration. * * @see DynamoDBMapper#load(Class, Object, Object, DynamoDBMapperConfig) */ public T load(Class clazz, Object hashKey) { return load(clazz, hashKey, null, config); } /** * Loads an object with a hash and range key, using the default * configuration. * * @see DynamoDBMapper#load(Class, Object, Object, DynamoDBMapperConfig) */ public T load(Class clazz, Object hashKey, Object rangeKey) { return load(clazz, hashKey, rangeKey, config); } /** * Returns an object with the given hash key, or null if no such object * exists. * * @param clazz * The class to load, corresponding to a DynamoDB table. * @param hashKey * The key of the object. * @param rangeKey * The range key of the object, or null for tables without a * range key. * @param config * Configuration for the service call to retrieve the object from * DynamoDB. This configuration overrides the default given at * construction. */ public T load(Class clazz, Object hashKey, Object rangeKey, DynamoDBMapperConfig config) { config = mergeConfig(config); String tableName = getTableName(clazz, config); // Fill in the hash key element in the service request Method hashKeyGetter = reflector.getHashKeyGetter(clazz); AttributeValue hashKeyElement = getHashKeyElement(hashKey, hashKeyGetter); // Determine the range key, if provided AttributeValue rangeKeyElement = null; if ( rangeKey != null ) { Method rangeKeyMethod = reflector.getRangeKeyGetter(clazz); if ( rangeKeyMethod == null ) { throw new DynamoDBMappingException("Zero-parameter range key property must be annotated with " + DynamoDBRangeKey.class); } rangeKeyElement = getRangeKeyElement(rangeKey, rangeKeyMethod); } GetItemResult item = db.getItem(applyUserAgent(new GetItemRequest().withTableName(tableName) .withKey(new Key().withHashKeyElement(hashKeyElement).withRangeKeyElement(rangeKeyElement)) .withConsistentRead(config.getConsistentReads() == ConsistentReads.CONSISTENT))); Map itemAttributes = item.getItem(); if ( itemAttributes == null ) { return null; } return marshallIntoObject(clazz, itemAttributes); } private String getTableName(Class clazz, DynamoDBMapperConfig config) { DynamoDBTable table = reflector.getTable(clazz); String tableName = table.tableName(); if ( config.getTableNameOverride() != null ) { if ( config.getTableNameOverride().getTableName() != null ) { tableName = config.getTableNameOverride().getTableName(); } else { tableName = config.getTableNameOverride().getTableNamePrefix() + tableName; } } return tableName; } private AttributeValue getHashKeyElement(Object hashKey, Method hashKeyGetter) { return getSimpleAttributeValue(hashKeyGetter, hashKey); } private AttributeValue getRangeKeyElement(Object rangeKey, Method rangeKeyGetter) { return getSimpleAttributeValue(rangeKeyGetter, rangeKey); } /** * Creates and fills in the attributes on an instance of the class given * with the attributes given. *

* This is accomplished by looking for getter methods annotated with an * appropriate annotation, then looking for matching attribute names in the * item attribute map. * * @param clazz * The class to instantiate and hydrate * @param itemAttributes * The set of item attributes, keyed by attribute name. */ public T marshallIntoObject(Class clazz, Map itemAttributes) { T toReturn = null; try { toReturn = clazz.newInstance(); } catch ( InstantiationException e ) { throw new DynamoDBMappingException("Failed to instantiate new instance of class", e); } catch ( IllegalAccessException e ) { throw new DynamoDBMappingException("Failed to instantiate new instance of class", e); } if ( itemAttributes == null || itemAttributes.isEmpty() ) return toReturn; itemAttributes = untransformAttributes(clazz, itemAttributes); for ( Method m : reflector.getRelevantGetters(clazz) ) { String attributeName = reflector.getAttributeName(m); if ( itemAttributes.containsKey(attributeName) ) { setValue(toReturn, m, itemAttributes.get(attributeName)); } } return toReturn; } /** * Marshalls the list of item attributes into objects of type clazz * * @see DynamoDBMapper#marshallIntoObject(Class, Map) */ public List marshallIntoObjects(Class clazz, List> itemAttributes) { List result = new ArrayList(); for (Map item : itemAttributes) { result.add(marshallIntoObject(clazz, item)); } return result; } /** * Sets the value in the return object corresponding to the service result. */ private void setValue(final T toReturn, final Method getter, AttributeValue value) { Method setter = reflector.getSetter(getter); ArgumentUnmarshaller unmarhsaller = reflector.getArgumentUnmarshaller(toReturn, getter, setter); unmarhsaller.typeCheck(value, setter); Object argument; try { argument = unmarhsaller.unmarshall(value); } catch ( IllegalArgumentException e ) { throw new DynamoDBMappingException("Couldn't unmarshall value " + value + " for " + setter, e); } catch ( ParseException e ) { throw new DynamoDBMappingException("Error attempting to parse date string " + value + " for "+ setter, e); } safeInvoke(setter, toReturn, argument); } /** * Returns an {@link AttributeValue} corresponding to the getter and return * result given, treating it as a non-versioned attribute. */ private AttributeValue getSimpleAttributeValue(final Method getter, final Object getterReturnResult) { if ( getterReturnResult == null ) return null; ArgumentMarshaller marshaller = reflector.getArgumentMarshaller(getter); return marshaller.marshall(getterReturnResult); } /** * Saves the object given into DynamoDB, using the default configuration. * * @see DynamoDBMapper#save(Object, DynamoDBMapperConfig) */ public void save(T object) { save(object, config); } /** * Saves an item in DynamoDB. The service method used is determined by the * {@link DynamoDBMapperConfig#getSaveBehavior()} value, to use either * {@link AmazonDynamoDB#putItem(PutItemRequest)} or * {@link AmazonDynamoDB#updateItem(UpdateItemRequest)}. For updates, a null * value for an object property will remove it from that item in DynamoDB. * For puts, a null value will not be passed to the service. The effect is * therefore the same, except when the item in DynamoDB contains attributes * that aren't modeled by the domain object given. * * @param object * The object to save into DynamoDB * @param config * The configuration to use, which overrides the default provided * at object construction. */ public void save(T object, DynamoDBMapperConfig config) { config = mergeConfig(config); @SuppressWarnings("unchecked") Class clazz = (Class) object.getClass(); String tableName = getTableName(clazz, config); Method hashKeyGetter = reflector.getHashKeyGetter(clazz); AttributeValue hashKeyElement = getHashKeyElement(safeInvoke(hashKeyGetter, object), hashKeyGetter); AttributeValue rangeKeyElement = null; Method rangeKeyGetter = reflector.getRangeKeyGetter(clazz); if ( rangeKeyGetter != null ) { rangeKeyElement = getRangeKeyElement(safeInvoke(rangeKeyGetter, object), rangeKeyGetter); } Key objectKey = new Key().withHashKeyElement(hashKeyElement).withRangeKeyElement(rangeKeyElement); Map updateValues = new HashMap(); Map expectedValues = new HashMap(); boolean forcePut = config.getSaveBehavior() == SaveBehavior.CLOBBER; boolean nonKeyAttributePresent = false; List inMemoryUpdates = new LinkedList(); /* * First look at the keys and construct updates for them independently */ List keyGetterMethods = new LinkedList(); keyGetterMethods.add(hashKeyGetter); if ( rangeKeyGetter != null ) { keyGetterMethods.add(rangeKeyGetter); } /* * Determine if there are any auto-assigned keys to assign. If so, force * a put and assign the keys */ if ( !forcePut ) { for ( Method method : keyGetterMethods ) { Object getterResult = safeInvoke(method, object); if ( getterResult == null && reflector.isAssignableKey(method) ) { forcePut = true; } } } /* * If we're doing a put, that means that we need update values for the * key attributes. */ if ( forcePut ) { for ( Method method : keyGetterMethods ) { Object getterResult = safeInvoke(method, object); String attributeName = reflector.getAttributeName(method); if ( getterResult == null && reflector.isAssignableKey(method) ) { AttributeValue newVersionValue = getAutoGeneratedKeyAttributeValue(method, getterResult); updateValues.put(attributeName, new AttributeValueUpdate().withAction("PUT").withValue(newVersionValue)); inMemoryUpdates.add(new ValueUpdate(method, newVersionValue, object)); if ( config.getSaveBehavior() != SaveBehavior.CLOBBER ) { // Add an expect clause to make sure that the item // doesn't already exist, since it's supposed to be new ExpectedAttributeValue expected = new ExpectedAttributeValue(); expected.setExists(false); expectedValues.put(attributeName, expected); } } else { AttributeValue currentValue = getSimpleAttributeValue(method, getterResult); if ( currentValue != null ) { updateValues.put(attributeName, new AttributeValueUpdate().withValue(currentValue).withAction("PUT")); } else { throw new DynamoDBMappingException("Null value for non-generated key for method " + method); } } } } else { /* * If we don't have the required keys by this point, it's an error */ if ( hashKeyElement == null ) { throw new DynamoDBMappingException("No value provided for hash key for object " + object); } if ( rangeKeyGetter != null && rangeKeyElement == null ) { throw new DynamoDBMappingException("No value provided for range key for object " + object); } } // Look at every getter and construct an update object for it for ( Method method : reflector.getRelevantGetters(clazz) ) { // Skip any key methods, since they are handled separately if ( method.equals(hashKeyGetter) || method.equals(rangeKeyGetter) ) continue; nonKeyAttributePresent = true; Object getterResult = safeInvoke(method, object); String attributeName = reflector.getAttributeName(method); /* * If this is a versioned field, update it */ if ( reflector.isVersionAttributeGetter(method) ) { if ( config.getSaveBehavior() != SaveBehavior.CLOBBER ) { // First establish the expected (current) value for the // update call ExpectedAttributeValue expected = new ExpectedAttributeValue(); // For new objects, insist that the value doesn't exist. // For existing ones, insist it has the old value. AttributeValue currentValue = getSimpleAttributeValue(method, getterResult); expected.setExists(currentValue != null); if ( currentValue != null ) { expected.setValue(currentValue); } expectedValues.put(attributeName, expected); } AttributeValue newVersionValue = getVersionAttributeValue(method, getterResult); updateValues .put(attributeName, new AttributeValueUpdate().withAction("PUT").withValue(newVersionValue)); inMemoryUpdates.add(new ValueUpdate(method, newVersionValue, object)); } /* * Otherwise apply the update value for this attribute. */ else { AttributeValue currentValue = getSimpleAttributeValue(method, getterResult); if ( currentValue != null ) { updateValues.put(attributeName, new AttributeValueUpdate().withValue(currentValue) .withAction("PUT")); } else if ( config.getSaveBehavior() != SaveBehavior.CLOBBER ) { updateValues.put(attributeName, new AttributeValueUpdate().withAction("DELETE")); } } } /* * Do a put or an update, according to the configuration. For a put (not * the default), we need to munge the data type. */ if ( config.getSaveBehavior() == SaveBehavior.CLOBBER || forcePut ) { db.putItem(applyUserAgent(new PutItemRequest().withTableName(tableName) .withItem(transformAttributes(clazz, convertToItem(updateValues))) .withExpected(expectedValues))); } else if ( !nonKeyAttributePresent ) { keyOnlyPut(clazz, tableName, objectKey, hashKeyGetter, rangeKeyGetter); } else { db.updateItem(applyUserAgent(new UpdateItemRequest().withTableName(tableName).withKey(objectKey) .withAttributeUpdates(transformAttributeUpdates(clazz, objectKey, updateValues)).withExpected(expectedValues))); } /* * Finally, after the service call has succeeded, update the in-memory * object with new field values as appropriate. */ for ( ValueUpdate update : inMemoryUpdates ) { update.apply(); } } /** * Edge case to deal with the problem reported here: * https://forums.aws.amazon.com/thread.jspa?threadID=86798&tstart=25 *

* DynamoDB cannot process an updateItem request that only contains the key * attribute(s), so we have to do a putItem. Somewhat confusingly, we also * insist that an item with the key(s) given doesn't already exist. This * isn't perfect, but we should be doing a putItem at all in this case, so * it's the best we can do. */ private void keyOnlyPut(Class clazz, String tableName, Key objectKey, Method hashKeyGetter, Method rangeKeyGetter) { Map attributes = new HashMap(); Map expectedValues = new HashMap(); String hashKeyAttributeName = reflector.getAttributeName(hashKeyGetter); attributes.put(hashKeyAttributeName, objectKey.getHashKeyElement()); expectedValues.put(hashKeyAttributeName, new ExpectedAttributeValue().withExists(false)); if (rangeKeyGetter != null) { String rangeKeyAttributeName = reflector.getAttributeName(rangeKeyGetter); attributes.put(rangeKeyAttributeName, objectKey.getRangeKeyElement()); expectedValues.put(rangeKeyAttributeName, new ExpectedAttributeValue().withExists(false)); } attributes = transformAttributes(clazz, attributes); db.putItem(applyUserAgent(new PutItemRequest().withTableName(tableName).withItem(attributes) .withExpected(expectedValues))); } /** * Deletes the given object from its DynamoDB table. */ public void delete(Object object) { delete(object, this.config); } /** * Deletes the given object from its DynamoDB table. * * @param config * Config override object. If {@link SaveBehavior#CLOBBER} is * supplied, version fields will not be considered when deleting * the object. */ public void delete(Object object, DynamoDBMapperConfig config) { config = mergeConfig(config); Class clazz = object.getClass(); String tableName = getTableName(clazz, config); Method hashKeyGetter = reflector.getHashKeyGetter(clazz); AttributeValue hashKeyElement = getHashKeyElement(safeInvoke(hashKeyGetter, object), hashKeyGetter); AttributeValue rangeKeyElement = null; Method rangeKeyGetter = reflector.getRangeKeyGetter(clazz); if ( rangeKeyGetter != null ) { rangeKeyElement = getRangeKeyElement(safeInvoke(rangeKeyGetter, object), rangeKeyGetter); } Key objectKey = new Key().withHashKeyElement(hashKeyElement).withRangeKeyElement(rangeKeyElement); /* * If there is a version field, make sure we assert its value. If the * version field is null (only should happen in unusual circumstances), * pretend it doesn't have a version field after all. */ Map expectedValues = new HashMap(); if ( config.getSaveBehavior() != SaveBehavior.CLOBBER ) { for ( Method method : reflector.getRelevantGetters(clazz) ) { if ( reflector.isVersionAttributeGetter(method) ) { Object getterResult = safeInvoke(method, object); String attributeName = reflector.getAttributeName(method); ExpectedAttributeValue expected = new ExpectedAttributeValue(); AttributeValue currentValue = getSimpleAttributeValue(method, getterResult); expected.setExists(currentValue != null); if ( currentValue != null ) expected.setValue(currentValue); expectedValues.put(attributeName, expected); break; } } } db.deleteItem(applyUserAgent(new DeleteItemRequest().withKey(objectKey).withTableName(tableName).withExpected(expectedValues))); } /** * Deletes the objects given using one or more calls to the * {@link AmazonDynamoDB#batchWriteItem(BatchWriteItemRequest)} API. * * @see DynamoDBMapper#batchWrite(List, List, DynamoDBMapperConfig) */ public void batchDelete(List objectsToDelete) { batchWrite(Collections.emptyList(), objectsToDelete, this.config); } /** * Deletes the objects given using one or more calls to the * {@link AmazonDynamoDB#batchWriteItem(BatchWriteItemRequest)} API. * * @see DynamoDBMapper#batchWrite(List, List, DynamoDBMapperConfig) */ public void batchDelete(Object... objectsToDelete) { batchWrite(Collections.emptyList(), Arrays.asList(objectsToDelete), this.config); } /** * Saves the objects given using one or more calls to the * {@link AmazonDynamoDB#batchWriteItem(BatchWriteItemRequest)} API. * * @see DynamoDBMapper#batchWrite(List, List, DynamoDBMapperConfig) */ public void batchSave(List objectsToSave) { batchWrite(objectsToSave, Collections.emptyList(), this.config); } /** * Saves the objects given using one or more calls to the * {@link AmazonDynamoDB#batchWriteItem(BatchWriteItemRequest)} API. * * @see DynamoDBMapper#batchWrite(List, List, DynamoDBMapperConfig) */ public void batchSave(Object... objectsToSave) { batchWrite(Arrays.asList(objectsToSave), Collections.emptyList(), this.config); } /** * Saves and deletes the objects given using one or more calls to the * {@link AmazonDynamoDB#batchWriteItem(BatchWriteItemRequest)} API. * * @see DynamoDBMapper#batchWrite(List, List, DynamoDBMapperConfig) */ public void batchWrite(List objectsToWrite, List objectsToDelete) { batchWrite(objectsToWrite, objectsToDelete, this.config); } /** * Saves and deletes the objects given using one or more calls to the * {@link AmazonDynamoDB#batchWriteItem(BatchWriteItemRequest)} API. * * @param objectsToWrite * A list of objects to save to DynamoDB. No version checks are * performed, as required by the * {@link AmazonDynamoDB#batchWriteItem(BatchWriteItemRequest)} * API. * @param objectsToDelete * A list of objects to delete from DynamoDB. No version checks * are performed, as required by the * {@link AmazonDynamoDB#batchWriteItem(BatchWriteItemRequest)} * API. * @param config * Only {@link DynamoDBMapperConfig#getTableNameOverride()} is * considered; if specified, all objects in the two parameter * lists will be considered to belong to the given table * override. */ public void batchWrite(List objectsToWrite, List objectsToDelete, DynamoDBMapperConfig config) { config = mergeConfig(config); HashMap> requestItems = new HashMap>(); List inMemoryUpdates = new LinkedList(); for ( Object toWrite : objectsToWrite ) { Class clazz = toWrite.getClass(); String tableName = getTableName(clazz, config); Map attributeValues = new HashMap(); // Look at every getter and construct a value object for it for ( Method method : reflector.getRelevantGetters(clazz) ) { Object getterResult = safeInvoke(method, toWrite); String attributeName = reflector.getAttributeName(method); AttributeValue currentValue = null; if ( getterResult == null && reflector.isAssignableKey(method) ) { currentValue = getAutoGeneratedKeyAttributeValue(method, getterResult); inMemoryUpdates.add(new ValueUpdate(method, currentValue, toWrite)); } else { currentValue = getSimpleAttributeValue(method, getterResult); } if ( currentValue != null ) { attributeValues.put(attributeName, currentValue); } } if ( !requestItems.containsKey(tableName) ) { requestItems.put(tableName, new LinkedList()); } requestItems.get(tableName).add( new WriteRequest().withPutRequest(new PutRequest().withItem(transformAttributes(clazz, attributeValues)))); } for ( Object toDelete : objectsToDelete ) { Class clazz = toDelete.getClass(); String tableName = getTableName(clazz, config); Method hashKeyGetter = reflector.getHashKeyGetter(clazz); AttributeValue hashKeyElement = getHashKeyElement(safeInvoke(hashKeyGetter, toDelete), hashKeyGetter); AttributeValue rangeKeyElement = null; Method rangeKeyGetter = reflector.getRangeKeyGetter(clazz); if ( rangeKeyGetter != null ) { rangeKeyElement = getRangeKeyElement(safeInvoke(rangeKeyGetter, toDelete), rangeKeyGetter); } Key objectKey = new Key().withHashKeyElement(hashKeyElement).withRangeKeyElement(rangeKeyElement); if ( !requestItems.containsKey(tableName) ) { requestItems.put(tableName, new LinkedList()); } requestItems.get(tableName).add( new WriteRequest().withDeleteRequest(new DeleteRequest().withKey(objectKey))); } // Break into chunks of 25 items and make service requests to DynamoDB while ( !requestItems.isEmpty() ) { HashMap> batch = new HashMap>(); int i = 0; Iterator>> tableIter = requestItems.entrySet().iterator(); while ( tableIter.hasNext() && i < 25 ) { Entry> tableRequest = tableIter.next(); batch.put(tableRequest.getKey(), new LinkedList()); Iterator writeRequestIter = tableRequest.getValue().iterator(); while ( writeRequestIter.hasNext() && i++ < 25 ) { WriteRequest writeRequest = writeRequestIter.next(); batch.get(tableRequest.getKey()).add(writeRequest); writeRequestIter.remove(); } // If we've processed all the write requests for this table, // remove it from the parent iterator. if ( !writeRequestIter.hasNext() ) { tableIter.remove(); } } BatchWriteItemResult result = db.batchWriteItem(new BatchWriteItemRequest().withRequestItems(batch)); // add any unprocessed items back into the list to process for ( Entry> unprocessedItem : result.getUnprocessedItems().entrySet() ) { if ( !requestItems.containsKey(unprocessedItem.getKey()) ) { requestItems.put(unprocessedItem.getKey(), new LinkedList()); } requestItems.get(unprocessedItem.getKey()).addAll(unprocessedItem.getValue()); } } // Once the entire batch is processed, update assigned keys in memory for ( ValueUpdate update : inMemoryUpdates ) { update.apply(); } } /** * Retrieves the attributes for multiple items from multiple tables using * their primary keys. * {@link AmazonDynamoDB#batchGetItem(BatchGetItemRequest)} API. * * @see #batchLoad(Map, DynamoDBMapperConfig) */ public Map> batchLoad(Map, List> itemsToGet) { return batchLoad(itemsToGet, this.config); } /** * * Retrieves the attributes for multiple items from multiple tables using * their primary keys. * {@link AmazonDynamoDB#batchGetItem(BatchGetItemRequest)} API. * * @param itemsToGet * Container for the necessary parameters to execute the * BatchGetItem service method on Amazon DynamoDB. * {@link AmazonDynamoDB#batchWriteItem(BatchWriteItemRequest)} * API. * @param config * Only {@link DynamoDBMapperConfig#getTableNameOverride()} is * considered; if specified, all objects in the two parameter * lists will be considered to belong to the given table * override. */ public Map> batchLoad(Map, List> itemsToGet, DynamoDBMapperConfig config) { config = mergeConfig(config); boolean consistentReads = (config.getConsistentReads() == ConsistentReads.CONSISTENT); if ( !validBatchGetRequest(itemsToGet) ) { return null; } Map requestItems = new HashMap(); Map> classesByTableName = new HashMap>(); Map> resultSet = new HashMap>(); int count = 0; for ( Class clazz : itemsToGet.keySet() ) { String tableName = getTableName(clazz, config); List keyPairs = itemsToGet.get(clazz); if ( keyPairs == null ) { continue; } classesByTableName.put(tableName, clazz); Method hashKeyGetter = reflector.getHashKeyGetter(clazz); for ( KeyPair keyPair : keyPairs ) { AttributeValue hashKeyElement = getHashKeyElement(keyPair.getHashKey(), hashKeyGetter); // Determine the range key, if provided AttributeValue rangeKeyElement = null; if ( keyPair.getRangeKey() != null ) { Method rangeKeyMethod = reflector.getRangeKeyGetter(clazz); if ( rangeKeyMethod == null ) { throw new DynamoDBMappingException("Zero-parameter range key property must be annotated with " + DynamoDBRangeKey.class); } rangeKeyElement = getRangeKeyElement(keyPair.getRangeKey(), rangeKeyMethod); } if ( !requestItems.containsKey(tableName) ) { requestItems .put(tableName, new KeysAndAttributes().withConsistentRead(consistentReads).withKeys( new LinkedList())); } requestItems.get(tableName).getKeys() .add(new Key().withHashKeyElement(hashKeyElement).withRangeKeyElement(rangeKeyElement)); // Reach the maximum number which can be handled in a single // batchGet if ( ++count == 100 ) { processBatchGetRequest(classesByTableName, requestItems, resultSet); requestItems.clear(); count = 0; } } } if ( count > 0 ) { processBatchGetRequest(classesByTableName, requestItems, resultSet); } return resultSet; } private void processBatchGetRequest(Map> tableNameToClassMapper, Map requestItems, Map> resultSet) { BatchGetItemResult batchGetItemResult = null; BatchGetItemRequest batchGetItemRequest = new BatchGetItemRequest(); batchGetItemRequest.setRequestItems(requestItems); do { if (batchGetItemResult != null) { batchGetItemRequest.setRequestItems(batchGetItemResult.getUnprocessedKeys()); } batchGetItemResult = db.batchGetItem(batchGetItemRequest); Map responses = batchGetItemResult.getResponses(); for (String tableName : responses.keySet()) { BatchResponse batchResponse = responses.get(tableName); List objects = null; if (resultSet.get(tableName) != null) { objects = resultSet.get(tableName); } else { objects = new LinkedList(); } List> items = batchResponse.getItems(); for (Map item : items) { objects.add(marshallIntoObject(tableNameToClassMapper.get(tableName), item)); } resultSet.put(tableName, objects); } // To see whether there are unprocessed keys. } while (batchGetItemResult.getUnprocessedKeys() != null && batchGetItemResult.getUnprocessedKeys().size() > 0); } /** * Check whether the batchGetRequest meet all the constraints. * * @param itemsToGet */ private boolean validBatchGetRequest(Map, List> itemsToGet) { if (itemsToGet == null || itemsToGet.size() == 0) { return false; } for (Class clazz : itemsToGet.keySet()) { if (itemsToGet.get(clazz) != null && itemsToGet.get(clazz).size() > 0) { return true; } } return false; } /** * Swallows the checked exceptions around Method.invoke and repackages them * as {@link DynamoDBMappingException} */ private Object safeInvoke(Method method, Object object, Object... arguments) { try { return method.invoke(object, arguments); } catch ( IllegalAccessException e ) { throw new DynamoDBMappingException("Couldn't invoke " + method, e); } catch ( IllegalArgumentException e ) { throw new DynamoDBMappingException("Couldn't invoke " + method, e); } catch ( InvocationTargetException e ) { throw new DynamoDBMappingException("Couldn't invoke " + method, e); } } private final class ValueUpdate { private Method method; private AttributeValue newValue; private Object target; public ValueUpdate(Method method, AttributeValue newValue, Object target) { this.method = method; this.newValue = newValue; this.target = target; } public void apply() { setValue(target, method, newValue); } } /** * Converts the {@link AttributeValueUpdate} map given to an equivalent * {@link AttributeValue} map. */ private Map convertToItem(Map putValues) { Map map = new HashMap(); for ( Entry entry : putValues.entrySet() ) { /* * AttributeValueUpdate allows nulls for its values, since they are * semantically meaningful. AttributeValues never have null values. */ if ( entry.getValue().getValue() != null ) map.put(entry.getKey(), entry.getValue().getValue()); } return map; } /** * Gets the attribute value object corresponding to the * {@link DynamoDBVersionAttribute} getter, and its result, given. Null * values are assumed to be new objects and given the smallest possible * positive value. Non-null values are incremented from their current value. */ private AttributeValue getVersionAttributeValue(final Method getter, Object getterReturnResult) { ArgumentMarshaller marshaller = reflector.getVersionedArgumentMarshaller(getter, getterReturnResult); return marshaller.marshall(getterReturnResult); } /** * Returns an attribute value corresponding to the key method and value given. */ private AttributeValue getAutoGeneratedKeyAttributeValue(Method getter, Object getterResult) { ArgumentMarshaller marshaller = reflector.getAutoGeneratedKeyArgumentMarshaller(getter); return marshaller.marshall(getterResult); } /** * Scans through an Amazon DynamoDB table and returns the matching results as * an unmodifiable list of instantiated objects, using the default configuration. * * @see DynamoDBMapper#scan(Class, DynamoDBScanExpression, DynamoDBMapperConfig) */ public PaginatedScanList scan(Class clazz, DynamoDBScanExpression scanExpression) { return scan(clazz, scanExpression, config); } /** * Scans through an Amazon DynamoDB table and returns the matching results as * an unmodifiable list of instantiated objects. The table to scan is * determined by looking at the annotations on the specified class, which * declares where to store the object data in Amazon DynamoDB, and the scan * expression parameter allows the caller to filter results and control how * the scan is executed. *

* Callers should be aware that the returned list is unmodifiable, and any * attempts to modify the list will result in an * UnsupportedOperationException. *

* The unmodifiable list returned is lazily loaded when possible, so calls * to DynamoDB will be made only as needed. * * @param * The type of the objects being returned. * @param clazz * The class annotated with DynamoDB annotations describing how * to store the object data in Amazon DynamoDB. * @param scanExpression * Details on how to run the scan, including any filters to apply * to limit results. * @param config * The configuration to use for this scan, which overrides the * default provided at object construction. * @return An unmodifiable list of the objects constructed from the results * of the scan operation. * @see PaginatedScanList */ public PaginatedScanList scan(Class clazz, DynamoDBScanExpression scanExpression, DynamoDBMapperConfig config) { config = mergeConfig(config); ScanRequest scanRequest = createScanRequestFromExpression(clazz, scanExpression, config); ScanResult scanResult = db.scan(applyUserAgent(scanRequest)); return new PaginatedScanList(this, clazz, db, scanRequest, scanResult); } /** * Scans through an Amazon DynamoDB table and returns a single page of matching * results. The table to scan is determined by looking at the annotations on * the specified class, which declares where to store the object data in AWS * DynamoDB, and the scan expression parameter allows the caller to filter * results and control how the scan is executed. * * @param * The type of the objects being returned. * @param clazz * The class annotated with DynamoDB annotations describing how * to store the object data in Amazon DynamoDB. * @param scanExpression * Details on how to run the scan, including any filters to apply * to limit results. * @param config * The configuration to use for this scan, which overrides the * default provided at object construction. */ public ScanResultPage scanPage(Class clazz, DynamoDBScanExpression scanExpression, DynamoDBMapperConfig config) { config = mergeConfig(config); ScanRequest scanRequest = createScanRequestFromExpression(clazz, scanExpression, config); ScanResult scanResult = db.scan(applyUserAgent(scanRequest)); ScanResultPage result = new ScanResultPage(); result.setResults(marshallIntoObjects(clazz, scanResult.getItems())); result.setLastEvaluatedKey(scanResult.getLastEvaluatedKey()); return result; } /** * Scans through an Amazon DynamoDB table and returns a single page of matching * results. * * @see DynamoDBMapper#scanPage(Class, DynamoDBScanExpression, DynamoDBMapperConfig) */ public ScanResultPage scanPage(Class clazz, DynamoDBScanExpression scanExpression) { return scanPage(clazz, scanExpression, this.config); } /** * Queries an Amazon DynamoDB table and returns the matching results as an * unmodifiable list of instantiated objects, using the default * configuration. * * @see DynamoDBMapper#query(Class, DynamoDBQueryExpression, * DynamoDBMapperConfig) */ public PaginatedQueryList query(Class clazz, DynamoDBQueryExpression queryExpression) { return query(clazz, queryExpression, config); } /** * Queries an Amazon DynamoDB table and returns the matching results as an * unmodifiable list of instantiated objects. The table to query is * determined by looking at the annotations on the specified class, which * declares where to store the object data in Amazon DynamoDB, and the query * expression parameter allows the caller to filter results and control how * the query is executed. *

* Callers should be aware that the returned list is unmodifiable, and any * attempts to modify the list will result in an * UnsupportedOperationException. *

* The unmodifiable list returned is lazily loaded when possible, so calls * to DynamoDB will be made only as needed. * * @param * The type of the objects being returned. * @param clazz * The class annotated with DynamoDB annotations describing how * to store the object data in Amazon DynamoDB. * @param queryExpression * Details on how to run the query, including any conditions on * the key values * @param config * The configuration to use for this query, which overrides the * default provided at object construction. * @return An unmodifiable list of the objects constructed from the results * of the query operation. * @see PaginatedQueryList */ public PaginatedQueryList query(Class clazz, DynamoDBQueryExpression queryExpression, DynamoDBMapperConfig config) { config = mergeConfig(config); QueryRequest queryRequest = createQueryRequestFromExpression(clazz, queryExpression, config); QueryResult queryResult = db.query(applyUserAgent(queryRequest)); return new PaginatedQueryList(this, clazz, db, queryRequest, queryResult); } /** * Queries an Amazon DynamoDB table and returns a single page of matching * results. The table to query is determined by looking at the annotations * on the specified class, which declares where to store the object data in * Amazon DynamoDB, and the query expression parameter allows the caller to * filter results and control how the query is executed. * * @see DynamoDBMapper#queryPage(Class, DynamoDBQueryExpression, DynamoDBMapperConfig) */ public QueryResultPage queryPage(Class clazz, DynamoDBQueryExpression queryExpression) { return queryPage(clazz, queryExpression, this.config); } /** * Queries an Amazon DynamoDB table and returns a single page of matching * results. The table to query is determined by looking at the annotations * on the specified class, which declares where to store the object data in * Amazon DynamoDB, and the query expression parameter allows the caller to * filter results and control how the query is executed. * * @param * The type of the objects being returned. * @param clazz * The class annotated with DynamoDB annotations describing how * to store the object data in AWS DynamoDB. * @param queryExpression * Details on how to run the query, including any conditions on * the key values * @param config * The configuration to use for this query, which overrides the * default provided at object construction. */ public QueryResultPage queryPage(Class clazz, DynamoDBQueryExpression queryExpression, DynamoDBMapperConfig config) { config = mergeConfig(config); QueryRequest queryRequest = createQueryRequestFromExpression(clazz, queryExpression, config); QueryResult scanResult = db.query(applyUserAgent(queryRequest)); QueryResultPage result = new QueryResultPage(); result.setResults(marshallIntoObjects(clazz, scanResult.getItems())); result.setLastEvaluatedKey(scanResult.getLastEvaluatedKey()); return result; } /** * Evaluates the specified scan expression and returns the count of matching * items, without returning any of the actual item data, using the default configuration. * * @see DynamoDBMapper#count(Class, DynamoDBScanExpression, DynamoDBMapperConfig) */ public int count(Class clazz, DynamoDBScanExpression scanExpression) { return count(clazz, scanExpression, config); } /** * Evaluates the specified scan expression and returns the count of matching * items, without returning any of the actual item data. *

* This operation will scan your entire table, and can therefore be very * expensive. Use with caution. * * @param clazz * The class mapped to a DynamoDB table. * @param scanExpression * The parameters for running the scan. * @param config * The configuration to use for this scan, which overrides the * default provided at object construction. * @return The count of matching items, without returning any of the actual * item data. */ public int count(Class clazz, DynamoDBScanExpression scanExpression, DynamoDBMapperConfig config) { config = mergeConfig(config); ScanRequest scanRequest = createScanRequestFromExpression(clazz, scanExpression, config); scanRequest.setCount(true); // Count scans can also be truncated for large datasets int count = 0; ScanResult scanResult = null; do { scanResult = db.scan(applyUserAgent(scanRequest)); count += scanResult.getCount(); scanRequest.setExclusiveStartKey(scanResult.getLastEvaluatedKey()); } while (scanResult.getLastEvaluatedKey() != null); return count; } /** * Evaluates the specified query expression and returns the count of matching * items, without returning any of the actual item data, using the default configuration. * * @see DynamoDBMapper#count(Class, DynamoDBQueryExpression, DynamoDBMapperConfig) */ public int count(Class clazz, DynamoDBQueryExpression queryExpression) { return count(clazz, queryExpression, config); } /** * Evaluates the specified query expression and returns the count of * matching items, without returning any of the actual item data. * * @param clazz * The class mapped to a DynamoDB table. * @param queryExpression * The parameters for running the scan. * @param config * The mapper configuration to use for the query, which overrides * the default provided at object construction. * @return The count of matching items, without returning any of the actual * item data. */ public int count(Class clazz, DynamoDBQueryExpression queryExpression, DynamoDBMapperConfig config) { config = mergeConfig(config); QueryRequest queryRequest = createQueryRequestFromExpression(clazz, queryExpression, config); queryRequest.setCount(true); // Count queries can also be truncated for large datasets int count = 0; QueryResult queryResult = null; do { queryResult = db.query(applyUserAgent(queryRequest)); count += queryResult.getCount(); queryRequest.setExclusiveStartKey(queryResult.getLastEvaluatedKey()); } while (queryResult.getLastEvaluatedKey() != null); return count; } /** * Merges the config object given with the one specified at construction and * returns the result. */ private DynamoDBMapperConfig mergeConfig(DynamoDBMapperConfig config) { if ( config != this.config ) config = new DynamoDBMapperConfig(this.config, config); return config; } private ScanRequest createScanRequestFromExpression(Class clazz, DynamoDBScanExpression scanExpression, DynamoDBMapperConfig config) { ScanRequest scanRequest = new ScanRequest(); scanRequest.setTableName(getTableName(clazz, config)); scanRequest.setScanFilter(scanExpression.getScanFilter()); scanRequest.setLimit(scanExpression.getLimit()); scanRequest.setExclusiveStartKey(scanExpression.getExclusiveStartKey()); return scanRequest; } private QueryRequest createQueryRequestFromExpression(Class clazz, DynamoDBQueryExpression queryExpression, DynamoDBMapperConfig config) { QueryRequest queryRequest = new QueryRequest(); queryRequest.setConsistentRead(queryExpression.isConsistentRead()); queryRequest.setTableName(getTableName(clazz, config)); queryRequest.setHashKeyValue(queryExpression.getHashKeyValue()); queryRequest.setScanIndexForward(queryExpression.isScanIndexForward()); queryRequest.setRangeKeyCondition(queryExpression.getRangeKeyCondition()); queryRequest.setLimit(queryExpression.getLimit()); queryRequest.setExclusiveStartKey(queryExpression.getExclusiveStartKey()); return queryRequest; } /** * By default, just calls {@link #untransformAttributes(String, String, Map)}. * @param clazz * @param attributeValues * @return the decrypted attribute values */ protected Map untransformAttributes(Class clazz, Map attributeValues) { Method hashKeyGetter = reflector.getHashKeyGetter(clazz); String hashKeyName = reflector.getAttributeName(hashKeyGetter); Method rangeKeyGetter = reflector.getRangeKeyGetter(clazz); String rangeKeyName = rangeKeyGetter == null ? null : reflector.getAttributeName(rangeKeyGetter); return untransformAttributes(hashKeyName, rangeKeyName, attributeValues); } /** * Transforms the attribute values after loading from DynamoDb. * Only ever called by {@link #untransformAttributes(Class, Map)}. * By default, returns the attributes unchanged. Currently, the * values associated with the hash key and range key must be left unchanged. * * @param hashKey the attribute name of the hash key * @param rangeKey the attribute name of the range key (or null if there is none) * @param attributeValues * @return the decrypted attributes */ protected Map untransformAttributes(String hashKey, String rangeKey, Map attributeValues) { return attributeValues; } /** * By default, just calls {@link #transformAttributes(String, String, Map)}. * @param clazz * @param attributeValues * @return the decrypted attribute values */ protected Map transformAttributes(Class clazz, Map attributeValues) { Method hashKeyGetter = reflector.getHashKeyGetter(clazz); String hashKeyName = reflector.getAttributeName(hashKeyGetter); Method rangeKeyGetter = reflector.getRangeKeyGetter(clazz); String rangeKeyName = rangeKeyGetter == null ? null : reflector.getAttributeName(rangeKeyGetter); return transformAttributes(hashKeyName, rangeKeyName, attributeValues); } /** * Transform attribute values prior to storing in DynamoDB. * Only ever called by {@link #transformAttributes(Class, Map)}. * By default, returns the attributes unchanged. * * @param hashKey the attribute name of the hash key * @param rangeKey the attribute name of the range key (or null if there is none) * @param attributeValues * @return the encrypted attributes */ protected Map transformAttributes(String hashKey, String rangeKey, Map attributeValues) { return attributeValues; } /** * A transformation expects to see all values, including keys, when * determining the transformation, therefore we must insert them if they are * not already present. However, we must remove the keys prior to actual * storage as this method is called when updating DynamoDB, which does * not permit the modification of key attributes as part of an update call. **/ private Map transformAttributeUpdates(Class clazz, Key objectKey, Map updateValues) { Map item = convertToItem(updateValues); boolean hashKeyAdded = false; boolean rangeKeyAdded = false; String hashKey = reflector.getAttributeName(reflector.getHashKeyGetter(clazz)); if(!item.containsKey(hashKey)) { item.put(hashKey, objectKey.getHashKeyElement()); hashKeyAdded = true; } String rangeKey = null; Method rangeKeyGetter = reflector.getRangeKeyGetter(clazz); if (rangeKeyGetter != null) { rangeKey = reflector.getAttributeName(rangeKeyGetter); if (!item.containsKey(rangeKey)) { item.put(rangeKey, objectKey.getRangeKeyElement()); rangeKeyAdded = true; } } item = transformAttributes(clazz, item); // Remove the keys if we added them before. if (hashKeyAdded) { item.remove(hashKey); } if (rangeKeyAdded) { item.remove(rangeKey); } for(String key: item.keySet()) { if (updateValues.containsKey(key)) { updateValues.get(key).getValue() .withB(item.get(key).getB()) .withBS(item.get(key).getBS()) .withN(item.get(key).getN()) .withNS(item.get(key).getNS()) .withS(item.get(key).getS()) .withSS(item.get(key).getSS()); } else { updateValues.put(key, new AttributeValueUpdate(item.get(key), "PUT")); } } return updateValues; } static X applyUserAgent(X request) { request.getRequestClientOptions().appendUserAgent(USER_AGENT); return request; } }