Java tutorial
/* * 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.dynamodbv2.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.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Random; import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import com.amazonaws.AmazonClientException; import com.amazonaws.AmazonServiceException; import com.amazonaws.AmazonWebServiceRequest; import com.amazonaws.auth.AWSCredentialsProvider; import com.amazonaws.retry.RetryUtils; import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperConfig.ConsistentReads; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperConfig.PaginationLoadingStrategy; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperConfig.SaveBehavior; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTableSchemaParser.TableIndexesInfo; import com.amazonaws.services.dynamodbv2.model.AttributeAction; import com.amazonaws.services.dynamodbv2.model.AttributeValue; import com.amazonaws.services.dynamodbv2.model.AttributeValueUpdate; import com.amazonaws.services.dynamodbv2.model.BatchGetItemRequest; import com.amazonaws.services.dynamodbv2.model.BatchGetItemResult; import com.amazonaws.services.dynamodbv2.model.BatchWriteItemRequest; import com.amazonaws.services.dynamodbv2.model.BatchWriteItemResult; import com.amazonaws.services.dynamodbv2.model.ComparisonOperator; import com.amazonaws.services.dynamodbv2.model.Condition; import com.amazonaws.services.dynamodbv2.model.ConditionalCheckFailedException; import com.amazonaws.services.dynamodbv2.model.CreateTableRequest; import com.amazonaws.services.dynamodbv2.model.ConditionalOperator; import com.amazonaws.services.dynamodbv2.model.DeleteItemRequest; import com.amazonaws.services.dynamodbv2.model.DeleteRequest; import com.amazonaws.services.dynamodbv2.model.ExpectedAttributeValue; import com.amazonaws.services.dynamodbv2.model.GetItemRequest; import com.amazonaws.services.dynamodbv2.model.GetItemResult; import com.amazonaws.services.dynamodbv2.model.KeysAndAttributes; import com.amazonaws.services.dynamodbv2.model.PutItemRequest; import com.amazonaws.services.dynamodbv2.model.PutItemResult; import com.amazonaws.services.dynamodbv2.model.PutRequest; import com.amazonaws.services.dynamodbv2.model.QueryRequest; import com.amazonaws.services.dynamodbv2.model.QueryResult; import com.amazonaws.services.dynamodbv2.model.ReturnValue; import com.amazonaws.services.dynamodbv2.model.ScanRequest; import com.amazonaws.services.dynamodbv2.model.ScanResult; import com.amazonaws.services.dynamodbv2.model.Select; import com.amazonaws.services.dynamodbv2.model.UpdateItemRequest; import com.amazonaws.services.dynamodbv2.model.UpdateItemResult; import com.amazonaws.services.dynamodbv2.model.WriteRequest; import com.amazonaws.services.s3.model.Region; import com.amazonaws.util.VersionInfoUtils; /** * Object mapper for domain-object interaction with DynamoDB. * <p> * To use, define a domain class that represents an item in a DynamoDB table and * annotate it with the annotations found in the * com.amazonaws.services.dynamodbv2.datamodeling package. In order to allow the * mapper to correctly persist the data, each modeled property in the domain * class should be accessible via getter and setter methods, and each property * annotation should be either applied to the getter method or the class field. * A minimal example using getter annotations: * * <pre class="brush: java"> * @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; * } * } * </pre> * <p> * Save instances of annotated classes to DynamoDB, retrieve them, and delete * them using the {@link DynamoDBMapper} class, as in the following example. * * <pre class="brush: java"> * 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); * </pre> * <p> * 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. * <p> * 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 */ public class DynamoDBMapper { private final S3ClientCache s3cc; private final AmazonDynamoDB db; private final DynamoDBMapperConfig config; private final DynamoDBReflector reflector = new DynamoDBReflector(); private final DynamoDBTableSchemaParser schemaParser = new DynamoDBTableSchemaParser(); private final AttributeTransformer transformer; /** The max back off time for batch write */ static final long MAX_BACKOFF_IN_MILLISECONDS = 1000 * 3; /** * This retry count is applicable only when every batch get item request * results in no data retrieved from server and the un processed keys is * same as request items */ static final int BATCH_GET_MAX_RETRY_COUNT_ALL_KEYS = 5; /** * User agent for requests made using the {@link DynamoDBMapper}. */ private static final String USER_AGENT = DynamoDBMapper.class.getName() + "/" + VersionInfoUtils.getVersion(); private static final String NO_RANGE_KEY = new String(); private static final Log log = LogFactory.getLog(DynamoDBMapper.class); /** * 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(final AmazonDynamoDB dynamoDB) { this(dynamoDB, DynamoDBMapperConfig.DEFAULT, null, null); } /** * 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(final AmazonDynamoDB dynamoDB, final DynamoDBMapperConfig config) { this(dynamoDB, config, null, null); } /** * Constructs a new mapper with the service object and S3 client cache * given, using the default configuration. * * @param ddb * The service object to use for all service calls. * @param s3CredentialProvider * The credentials provider for accessing S3. * Relevant only if {@link S3Link} is involved. * @see DynamoDBMapperConfig#DEFAULT */ public DynamoDBMapper(final AmazonDynamoDB ddb, final AWSCredentialsProvider s3CredentialProvider) { this(ddb, DynamoDBMapperConfig.DEFAULT, s3CredentialProvider); } /** * Constructs a new mapper with the given service object, configuration, * and transform hook. * * @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 * @param transformer * The custom attribute transformer to invoke when serializing or * deserializing an object. */ public DynamoDBMapper(final AmazonDynamoDB dynamoDB, final DynamoDBMapperConfig config, final AttributeTransformer transformer) { this(dynamoDB, config, transformer, null); } /** * Constructs a new mapper with the service object, configuration, and S3 * client cache 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. * @param s3CredentialProvider * The credentials provider for accessing S3. * Relevant only if {@link S3Link} is involved. */ public DynamoDBMapper(final AmazonDynamoDB dynamoDB, final DynamoDBMapperConfig config, final AWSCredentialsProvider s3CredentialProvider) { this(dynamoDB, config, null, validate(s3CredentialProvider)); } /** * Throws an exception if the given credentials provider is {@code null}. */ private static AWSCredentialsProvider validate(final AWSCredentialsProvider provider) { if (provider == null) { throw new IllegalArgumentException("s3 credentials provider must not be null"); } return provider; } /** * Constructor with all parameters. * * @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. * @param transformer * The custom attribute transformer to invoke when serializing or * deserializing an object. * @param s3CredentialProvider * The credentials provider for accessing S3. * Relevant only if {@link S3Link} is involved. */ public DynamoDBMapper(final AmazonDynamoDB dynamoDB, final DynamoDBMapperConfig config, final AttributeTransformer transformer, final AWSCredentialsProvider s3CredentialsProvider) { this.db = dynamoDB; this.config = config; this.transformer = transformer; if (s3CredentialsProvider == null) { this.s3cc = null; } else { this.s3cc = new S3ClientCache(s3CredentialsProvider.getCredentials()); } } /** * 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 extends Object> T load(Class<T> 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 extends Object> T load(Class<T> 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 extends Object> T load(Class<T> clazz, Object hashKey, Object rangeKey) { return load(clazz, hashKey, rangeKey, config); } /** * Returns an object whose keys match those of the prototype key object given, * or null if no such item exists. * * @param keyObject * An object of the class to load with the keys values to match. * * @see DynamoDBMapper#load(Object, DynamoDBMapperConfig) */ public <T extends Object> T load(T keyObject) { return load(keyObject, this.config); } /** * Returns an object whose keys match those of the prototype key object given, * or null if no such item exists. * * @param keyObject * An object of the class to load with the keys values to match. * @param config * Configuration for the service call to retrieve the object from * DynamoDB. This configuration overrides the default given at * construction. */ public <T extends Object> T load(T keyObject, DynamoDBMapperConfig config) { @SuppressWarnings("unchecked") Class<T> clazz = (Class<T>) keyObject.getClass(); config = mergeConfig(config); String tableName = getTableName(clazz, config); GetItemRequest rq = new GetItemRequest().withRequestMetricCollector(config.getRequestMetricCollector()); Map<String, AttributeValue> key = getKey(keyObject, clazz); rq.setKey(key); rq.setTableName(tableName); rq.setConsistentRead(config.getConsistentReads() == ConsistentReads.CONSISTENT); GetItemResult item = db.getItem(applyUserAgent(rq)); Map<String, AttributeValue> itemAttributes = item.getItem(); if (itemAttributes == null) { return null; } T object = marshalIntoObject(toParameters(itemAttributes, clazz, config)); return object; } /** * Returns a key map for the key object given. * * @param keyObject * The key object, corresponding to an item in a dynamo table. */ @SuppressWarnings("unchecked") private <T> Map<String, AttributeValue> getKey(T keyObject) { return getKey(keyObject, (Class<T>) keyObject.getClass()); } private <T> Map<String, AttributeValue> getKey(T keyObject, Class<T> clazz) { Map<String, AttributeValue> key = new HashMap<String, AttributeValue>(); for (Method keyGetter : reflector.getPrimaryKeyGetters(clazz)) { Object getterResult = safeInvoke(keyGetter, keyObject); AttributeValue keyAttributeValue = getSimpleAttributeValue(keyGetter, getterResult); if (keyAttributeValue == null) { throw new DynamoDBMappingException("Null key found for " + keyGetter); } key.put(reflector.getAttributeName(keyGetter), keyAttributeValue); } if (key.isEmpty()) { throw new DynamoDBMappingException( "Class must be annotated with " + DynamoDBHashKey.class + " and " + DynamoDBRangeKey.class); } return key; } /** * 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 extends Object> T load(Class<T> clazz, Object hashKey, Object rangeKey, DynamoDBMapperConfig config) { config = mergeConfig(config); T keyObject = createKeyObject(clazz, hashKey, rangeKey); return load(keyObject, config); } /** * Creates a key prototype object for the class given with the single hash and range key given. */ private <T> T createKeyObject(Class<T> clazz, Object hashKey, Object rangeKey) { T keyObject = null; try { keyObject = clazz.newInstance(); } catch (Exception e) { throw new DynamoDBMappingException("Failed to instantiate class", e); } boolean seenHashKey = false; boolean seenRangeKey = false; for (Method getter : reflector.getPrimaryKeyGetters(clazz)) { if (ReflectionUtils.getterOrFieldHasAnnotation(getter, DynamoDBHashKey.class)) { if (seenHashKey) { throw new DynamoDBMappingException("Found more than one method annotated with " + DynamoDBHashKey.class + " for class " + clazz + ". Use load(Object) for tables with more than a single hash and range key."); } seenHashKey = true; safeInvoke(reflector.getSetter(getter), keyObject, hashKey); } else if (ReflectionUtils.getterOrFieldHasAnnotation(getter, DynamoDBRangeKey.class)) { if (seenRangeKey) { throw new DynamoDBMappingException("Found more than one method annotated with " + DynamoDBRangeKey.class + " for class " + clazz + ". Use load(Object) for tables with more than a single hash and range key."); } seenRangeKey = true; safeInvoke(reflector.getSetter(getter), keyObject, rangeKey); } } if (!seenHashKey) { throw new DynamoDBMappingException( "No method annotated with " + DynamoDBHashKey.class + " for class " + clazz + "."); } else if (rangeKey != null && !seenRangeKey) { throw new DynamoDBMappingException( "No method annotated with " + DynamoDBRangeKey.class + " for class " + clazz + "."); } return keyObject; } /** * Returns a map of attribute name to EQ condition for the key prototype * object given. This method considers attributes annotated with either * {@link DynamoDBHashKey} or {@link DynamoDBIndexHashKey}. * * @param obj * The prototype object that includes the hash key value. * @return A map of hash key attribute name to EQ condition for the key * prototype object, or an empty map if obj is null. */ private Map<String, Condition> getHashKeyEqualsConditions(Object obj) { Map<String, Condition> conditions = new HashMap<String, Condition>(); if (obj != null) { for (Method getter : reflector.getRelevantGetters(obj.getClass())) { if (ReflectionUtils.getterOrFieldHasAnnotation(getter, DynamoDBHashKey.class) || ReflectionUtils.getterOrFieldHasAnnotation(getter, DynamoDBIndexHashKey.class)) { Object getterReturnResult = safeInvoke(getter, obj, (Object[]) null); if (getterReturnResult != null) { conditions.put(reflector.getAttributeName(getter), new Condition().withComparisonOperator(ComparisonOperator.EQ) .withAttributeValueList( getSimpleAttributeValue(getter, getterReturnResult))); } } } } return conditions; } /** * Returns the table name for the class given. */ protected final String getTableName(final Class<?> clazz, final DynamoDBMapperConfig config) { return getTableName(clazz, config, reflector); } static String getTableName(final Class<?> clazz, final DynamoDBMapperConfig config, final DynamoDBReflector reflector) { 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; } /** * A replacement for {@link #marshallIntoObject(Class, Map)} that takes * extra parameters to tunnel through to {@code privateMarshalIntoObject}. * <p> * Once {@code marshallIntoObject} is removed, this method will directly * call {@code privateMarshalIntoObject}. */ private <T> T marshalIntoObject(final AttributeTransformer.Parameters<T> parameters) { return marshallIntoObject(parameters.getModelClass(), MapAnd.wrap(parameters.getAttributeValues(), parameters)); } /** * Creates and fills in the attributes on an instance of the class given * with the attributes given. * <p> * This is accomplished by looking for getter methods annotated with an * appropriate annotation, then looking for matching attribute names in the * item attribute map. * <p> * This method has been marked deprecated because it does not allow * load/query/scan to pass through their DynamoDBMapperConfig parameter, * which is needed by some implementations of {@code AttributeTransformer}. * In a future version of the SDK, load/query/scan will be changed to * directly call privateMarshalIntoObject, and will no longer call this * method. * <p> * If you are extending DynamoDBMapper and overriding this method to * customize how the mapper unmarshals POJOs from a raw DynamoDB item, * please switch to using an AttributeTransformer (or open a GitHub * issue if you need to fully control the unmarshalling process, and we'll * figure out a better way to expose such a hook). * <p> * If you're simply calling this method, it will continue to be available * for the forseeable future - feel free to ignore the @Deprecated tag. * * @param clazz * The class to instantiate and hydrate * @param itemAttributes * The set of item attributes, keyed by attribute name. * @deprecated as an extension point for adding custom unmarshalling */ @Deprecated public <T> T marshallIntoObject(Class<T> clazz, Map<String, AttributeValue> itemAttributes) { if (itemAttributes instanceof MapAnd) { @SuppressWarnings("unchecked") AttributeTransformer.Parameters<T> parameters = ((MapAnd<?, ?, AttributeTransformer.Parameters<T>>) itemAttributes) .getExtra(); return privateMarshalIntoObject(parameters); } else { // Called via some unexpected external codepath; use the class-level // config. return privateMarshalIntoObject(toParameters(itemAttributes, clazz, this.config)); } } /** * The one true implementation of marshalIntoObject. */ private <T> T privateMarshalIntoObject(final AttributeTransformer.Parameters<T> parameters) { T toReturn = null; try { toReturn = parameters.getModelClass().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 (parameters.getAttributeValues() == null || parameters.getAttributeValues().isEmpty()) { return toReturn; } Map<String, AttributeValue> result = untransformAttributes(parameters); for (Method m : reflector.getRelevantGetters(parameters.getModelClass())) { String attributeName = reflector.getAttributeName(m); if (result.containsKey(attributeName)) { setValue(toReturn, m, result.get(attributeName)); } } return toReturn; } /** * Unmarshalls the list of item attributes into objects of type clazz. * <p> * This method has been marked deprecated because it does not allow * query/scan to pass through their DynamoDBMapperConfig parameter, * which is needed by some implementations of {@code AttributeTransformer}. * In a future version of the SDK, query/scan will be changed to directly * call privateMarshalIntoObjects, and will no longer call this method. * <p> * If you are extending DynamoDBMapper and overriding this method to * customize how the mapper unmarshals POJOs from raw DynamoDB items, * please switch to using an AttributeTransformer (or open a GitHub * issue if you need to fully control the unmarshalling process, and we'll * figure out a better way to expose such a hook). * <p> * If you're simply calling this method, it will continue to be available * for the forseeable future - feel free to ignore the @Deprecated tag. * * @see DynamoDBMapper#marshallIntoObject(Class, Map) * @deprecated as an extension point for adding custom unmarshalling */ @Deprecated public <T> List<T> marshallIntoObjects(Class<T> clazz, List<Map<String, AttributeValue>> itemAttributes) { List<T> result = new ArrayList<T>(itemAttributes.size()); for (Map<String, AttributeValue> item : itemAttributes) { result.add(marshallIntoObject(clazz, item)); } return result; } /** * A replacement for {@link #marshallIntoObjects(Class, List)} that takes * an extra set of parameters to be tunneled through to * {@code privateMarshalIntoObject} (if nothing along the way is * overridden). It's package-private because some of the Paginated*List * classes call back into it, but final because no one, even in this * package, should ever override it. * <p> * In the future, when the deprecated {@code marshallIntoObjects} is * removed, this method will be changed to directly call * {@code privateMarshalIntoObject}. */ final <T> List<T> marshalIntoObjects(final List<AttributeTransformer.Parameters<T>> parameters) { if (parameters.isEmpty()) { return Collections.emptyList(); } Class<T> clazz = parameters.get(0).getModelClass(); List<Map<String, AttributeValue>> list = new ArrayList<Map<String, AttributeValue>>(parameters.size()); for (AttributeTransformer.Parameters<T> entry : parameters) { list.add(MapAnd.wrap(entry.getAttributeValues(), entry)); } return marshallIntoObjects(clazz, list); } /** * Sets the value in the return object corresponding to the service result. */ private <T> void setValue(final T toReturn, final Method getter, AttributeValue value) { Method setter = reflector.getSetter(getter); ArgumentUnmarshaller unmarhsaller = reflector.getArgumentUnmarshaller(toReturn, getter, setter, s3cc); 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, DynamoDBSaveExpression, DynamoDBMapperConfig) */ public <T extends Object> void save(T object) { save(object, null, config); } /** * Saves the object given into DynamoDB, using the default configuration and the specified saveExpression. * * @see DynamoDBMapper#save(Object, DynamoDBSaveExpression, DynamoDBMapperConfig) */ public <T extends Object> void save(T object, DynamoDBSaveExpression saveExpression) { save(object, saveExpression, config); } private boolean needAutoGenerateAssignableKey(Class<?> clazz, Object object) { Collection<Method> keyGetters = reflector.getPrimaryKeyGetters(clazz); boolean forcePut = false; /* * Determine if there are any auto-assigned keys to assign. If so, force * a put and assign the keys. */ boolean hashKeyGetterFound = false; for (Method method : keyGetters) { Object getterResult = safeInvoke(method, object); if (getterResult == null && reflector.isAssignableKey(method)) { forcePut = true; } if (ReflectionUtils.getterOrFieldHasAnnotation(method, DynamoDBHashKey.class)) { hashKeyGetterFound = true; } } if (!hashKeyGetterFound) { throw new DynamoDBMappingException( "No " + DynamoDBHashKey.class + " annotation found in class " + clazz); } return forcePut; } /** * Saves the object given into DynamoDB, using the specified configuration. * * @see DynamoDBMapper#save(Object, DynamoDBSaveExpression, DynamoDBMapperConfig) */ public <T extends Object> void save(T object, DynamoDBMapperConfig config) { save(object, null, 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)}: * <ul> * <li><b>UPDATE</b> (default) : UPDATE will not affect unmodeled attributes * on a save operation and a null value for the modeled attribute will * remove it from that item in DynamoDB. Because of the limitation of * updateItem request, the implementation of UPDATE will send a putItem * request when a key-only object is being saved, and it will send another * updateItem request if the given key(s) already exists in the table.</li> * <li><b>UPDATE_SKIP_NULL_ATTRIBUTES</b> : Similar to UPDATE except that it * ignores any null value attribute(s) and will NOT remove them from that * item in DynamoDB. It also guarantees to send only one single updateItem * request, no matter the object is key-only or not.</li> * <li><b>CLOBBER</b> : CLOBBER will clear and replace all attributes, * included unmodeled ones, (delete and recreate) on save. Versioned field * constraints will also be disregarded.</li> * </ul> * * * Any options specified in the saveExpression parameter will be overlaid on * any constraints due to versioned attributes. * * @param object * The object to save into DynamoDB * @param saveExpression * The options to apply to this save request * @param config * The configuration to use, which overrides the default provided * at object construction. * * @see DynamoDBMapperConfig.SaveBehavior */ public <T extends Object> void save(T object, DynamoDBSaveExpression saveExpression, final DynamoDBMapperConfig config) { final DynamoDBMapperConfig finalConfig = mergeConfig(config); @SuppressWarnings("unchecked") Class<? extends T> clazz = (Class<? extends T>) object.getClass(); String tableName = getTableName(clazz, finalConfig); /* * We force a putItem request instead of updateItem request either when * CLOBBER is configured, or part of the primary key of the object needs * to be auto-generated. */ boolean forcePut = (finalConfig.getSaveBehavior() == SaveBehavior.CLOBBER) || needAutoGenerateAssignableKey(clazz, object); SaveObjectHandler saveObjectHandler; if (forcePut) { saveObjectHandler = this.new SaveObjectHandler(clazz, object, tableName, finalConfig, saveExpression) { @Override protected void onKeyAttributeValue(String attributeName, AttributeValue keyAttributeValue) { /* Treat key values as common attribute value updates. */ getAttributeValueUpdates().put(attributeName, new AttributeValueUpdate().withValue(keyAttributeValue).withAction("PUT")); } /* Use default implementation of onNonKeyAttribute(...) */ @Override protected void onNullNonKeyAttribute(String attributeName) { /* When doing a force put, we can safely ignore the null-valued attributes. */ return; } @Override protected void executeLowLevelRequest() { /* Send a putItem request */ doPutItem(); } }; } else { saveObjectHandler = this.new SaveObjectHandler(clazz, object, tableName, finalConfig, saveExpression) { @Override protected void onKeyAttributeValue(String attributeName, AttributeValue keyAttributeValue) { /* Put it in the key collection which is later used in the updateItem request. */ getKeyAttributeValues().put(attributeName, keyAttributeValue); } @Override protected void onNonKeyAttribute(String attributeName, AttributeValue currentValue) { /* If it's a set attribute and the mapper is configured with APPEND_SET, * we do an "ADD" update instead of the default "PUT". */ if (getLocalSaveBehavior() == SaveBehavior.APPEND_SET) { if (currentValue.getBS() != null || currentValue.getNS() != null || currentValue.getSS() != null) { getAttributeValueUpdates().put(attributeName, new AttributeValueUpdate().withValue(currentValue).withAction("ADD")); return; } } /* Otherwise, we do the default "PUT" update. */ super.onNonKeyAttribute(attributeName, currentValue); } @Override protected void onNullNonKeyAttribute(String attributeName) { /* * If UPDATE_SKIP_NULL_ATTRIBUTES or APPEND_SET is * configured, we don't delete null value attributes. */ if (getLocalSaveBehavior() == SaveBehavior.UPDATE_SKIP_NULL_ATTRIBUTES || getLocalSaveBehavior() == SaveBehavior.APPEND_SET) { return; } else { /* Delete attributes that are set as null in the object. */ getAttributeValueUpdates().put(attributeName, new AttributeValueUpdate().withAction("DELETE")); } } @Override protected void executeLowLevelRequest() { UpdateItemResult updateItemResult = doUpdateItem(); // The UpdateItem request is specified to return ALL_NEW // attributes of the affected item. So if the returned // UpdateItemResult does not include any ReturnedAttributes, // it indicates the UpdateItem failed silently (e.g. the // key-only-put nightmare - // https://forums.aws.amazon.com/thread.jspa?threadID=86798&tstart=25), // in which case we should re-send a PutItem // request instead. if (updateItemResult.getAttributes() == null || updateItemResult.getAttributes().isEmpty()) { // Before we proceed with PutItem, we need to put all // the key attributes (prepared for the // UpdateItemRequest) into the AttributeValueUpdates // collection. for (String keyAttributeName : getKeyAttributeValues().keySet()) { getAttributeValueUpdates().put(keyAttributeName, new AttributeValueUpdate() .withValue(getKeyAttributeValues().get(keyAttributeName)).withAction("PUT")); } doPutItem(); } } }; } saveObjectHandler.execute(); } /** * The handler for saving object using DynamoDBMapper. Caller should * implement the abstract methods to provide the expected behavior on each * scenario, and this handler will take care of all the other basic workflow * and common operations. */ protected abstract class SaveObjectHandler { protected final Object object; protected final Class<?> clazz; private final String tableName; private final DynamoDBMapperConfig saveConfig; private final Map<String, AttributeValue> key; private final Map<String, AttributeValueUpdate> updateValues; /** * Any expected value conditions specified by the implementation of * DynamoDBMapper, e.g. value assertions on versioned attributes. */ private final Map<String, ExpectedAttributeValue> internalExpectedValueAssertions; /** * Additional expected value conditions specified by the user. */ protected final Map<String, ExpectedAttributeValue> userProvidedExpectedValueConditions; /** * Condition operator on the additional expected value conditions specified by the user. */ protected final String userProvidedConditionOperator; private final List<ValueUpdate> inMemoryUpdates; /** * Constructs a handler for saving the specified model object. * * @param object The model object to be saved. * @param clazz The domain class of the object. * @param tableName The table name. * @param saveConifg The mapper configuration used for this save. * @param saveExpression The save expression, including the user-provided conditions and an optional logic operator. */ public SaveObjectHandler(Class<?> clazz, Object object, String tableName, DynamoDBMapperConfig saveConfig, DynamoDBSaveExpression saveExpression) { this.clazz = clazz; this.object = object; this.tableName = tableName; this.saveConfig = saveConfig; if (saveExpression != null) { userProvidedExpectedValueConditions = saveExpression.getExpected(); userProvidedConditionOperator = saveExpression.getConditionalOperator(); } else { userProvidedExpectedValueConditions = null; userProvidedConditionOperator = null; } updateValues = new HashMap<String, AttributeValueUpdate>(); internalExpectedValueAssertions = new HashMap<String, ExpectedAttributeValue>(); inMemoryUpdates = new LinkedList<ValueUpdate>(); key = new HashMap<String, AttributeValue>(); } /** * The general workflow of a save operation. */ public void execute() { Collection<Method> keyGetters = reflector.getPrimaryKeyGetters(clazz); /* * First handle keys */ for (Method method : keyGetters) { Object getterResult = safeInvoke(method, object); String attributeName = reflector.getAttributeName(method); if (getterResult == null && reflector.isAssignableKey(method)) { onAutoGenerateAssignableKey(method, attributeName); } else { AttributeValue newAttributeValue = getSimpleAttributeValue(method, getterResult); if (newAttributeValue == null) { throw new DynamoDBMappingException("Null or empty value for key: " + method); } onKeyAttributeValue(attributeName, newAttributeValue); } } /* * Next construct an update for every non-key property */ for (Method method : reflector.getRelevantGetters(clazz)) { // Skip any key methods, since they are handled separately if (keyGetters.contains(method)) continue; Object getterResult = safeInvoke(method, object); String attributeName = reflector.getAttributeName(method); /* * If this is a versioned field, update it */ if (reflector.isVersionAttributeGetter(method)) { onVersionAttribute(method, getterResult, attributeName); } /* * Otherwise apply the update value for this attribute. */ else { AttributeValue currentValue = getSimpleAttributeValue(method, getterResult); if (currentValue != null) { onNonKeyAttribute(attributeName, currentValue); } else { onNullNonKeyAttribute(attributeName); } } } /* * Execute the implementation of the low level request. */ executeLowLevelRequest(); /* * Finally, after the service call has succeeded, update the * in-memory object with new field values as appropriate. This * currently takes into account of auto-generated keys and versioned * attributes. */ for (ValueUpdate update : inMemoryUpdates) { update.apply(); } } /** * Implement this method to do the necessary operations when a key * attribute is set with some value. * * @param attributeName * The name of the key attribute. * @param keyAttributeValue * The AttributeValue of the key attribute as specified in * the object. */ protected abstract void onKeyAttributeValue(String attributeName, AttributeValue keyAttributeValue); /** * Implement this method for necessary operations when a non-key * attribute is set a non-null value in the object. * The default implementation simply adds a "PUT" update for the given attribute. * * @param attributeName * The name of the non-key attribute. * @param currentValue * The updated value of the given attribute. */ protected void onNonKeyAttribute(String attributeName, AttributeValue currentValue) { updateValues.put(attributeName, new AttributeValueUpdate().withValue(currentValue).withAction("PUT")); } /** * Implement this method for necessary operations when a non-key * attribute is set null in the object. * * @param attributeName * The name of the non-key attribute. */ protected abstract void onNullNonKeyAttribute(String attributeName); /** * Implement this method to send the low-level request that is necessary * to complete the save operation. */ protected abstract void executeLowLevelRequest(); /** Get the SaveBehavior used locally for this save operation. **/ protected SaveBehavior getLocalSaveBehavior() { return saveConfig.getSaveBehavior(); } /** Get the table name **/ protected String getTableName() { return tableName; } /** Get the map of all the specified key of the saved object. **/ protected Map<String, AttributeValue> getKeyAttributeValues() { return key; } /** Get the map of AttributeValueUpdate on each modeled attribute. **/ protected Map<String, AttributeValueUpdate> getAttributeValueUpdates() { return updateValues; } /** * Merge and return all the expected value conditions (either * user-specified or imposed by the internal implementation of * DynamoDBMapper) for this save operation. */ protected Map<String, ExpectedAttributeValue> mergeExpectedAttributeValueConditions() { return DynamoDBMapper.mergeExpectedAttributeValueConditions(internalExpectedValueAssertions, userProvidedExpectedValueConditions, userProvidedConditionOperator); } /** Get the list of all the necessary in-memory update on the object. **/ protected List<ValueUpdate> getInMemoryUpdates() { return inMemoryUpdates; } /** * Save the item using a UpdateItem request. The handler will call this * method if * <ul> * <li>CLOBBER configuration is not being used; * <li>AND the item does not contain auto-generated key value; * </ul> * <p> * The ReturnedValues parameter for the UpdateItem request is set as * ALL_NEW, which means the service should return all of the attributes * of the new version of the item after the update. The handler will use * the returned attributes to detect silent failure on the server-side. */ protected UpdateItemResult doUpdateItem() { UpdateItemRequest req = new UpdateItemRequest().withTableName(getTableName()) .withKey(getKeyAttributeValues()) .withAttributeUpdates(transformAttributeUpdates(this.clazz, getKeyAttributeValues(), getAttributeValueUpdates(), saveConfig)) .withExpected(mergeExpectedAttributeValueConditions()) .withConditionalOperator(userProvidedConditionOperator).withReturnValues(ReturnValue.ALL_NEW) .withRequestMetricCollector(saveConfig.getRequestMetricCollector()); return db.updateItem(applyUserAgent(req)); } /** * Save the item using a PutItem request. The handler will call this * method if * <ul> * <li> CLOBBER configuration is being used; * <li> OR the item contains auto-generated key value; * <li> OR an UpdateItem request has silently failed (200 response with * no affected attribute), which indicates the key-only-put scenario * that we used to handle by the keyOnlyPut(...) hack. * </ul> */ protected PutItemResult doPutItem() { Map<String, AttributeValue> attributeValues = convertToItem(getAttributeValueUpdates()); attributeValues = transformAttributes(toParameters(attributeValues, this.clazz, saveConfig)); PutItemRequest req = new PutItemRequest().withTableName(getTableName()).withItem(attributeValues) .withExpected(mergeExpectedAttributeValueConditions()) .withConditionalOperator(userProvidedConditionOperator) .withRequestMetricCollector(saveConfig.getRequestMetricCollector()); return db.putItem(applyUserAgent(req)); } private void onAutoGenerateAssignableKey(Method method, String attributeName) { AttributeValue newVersionValue = getAutoGeneratedKeyAttributeValue(method, null); updateValues.put(attributeName, new AttributeValueUpdate().withAction("PUT").withValue(newVersionValue)); inMemoryUpdates.add(new ValueUpdate(method, newVersionValue, object)); if (getLocalSaveBehavior() != SaveBehavior.CLOBBER && !internalExpectedValueAssertions.containsKey(attributeName)) { // 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); internalExpectedValueAssertions.put(attributeName, expected); } } private void onVersionAttribute(Method method, Object getterResult, String attributeName) { if (getLocalSaveBehavior() != SaveBehavior.CLOBBER && !internalExpectedValueAssertions.containsKey(attributeName)) { // 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); } internalExpectedValueAssertions.put(attributeName, expected); } AttributeValue newVersionValue = getVersionAttributeValue(method, getterResult); updateValues.put(attributeName, new AttributeValueUpdate().withAction("PUT").withValue(newVersionValue)); inMemoryUpdates.add(new ValueUpdate(method, newVersionValue, object)); } } /** * Deletes the given object from its DynamoDB table using the default configuration. */ public void delete(Object object) { delete(object, null, this.config); } /** * Deletes the given object from its DynamoDB table using the specified deleteExpression and default configuration. */ public void delete(Object object, DynamoDBDeleteExpression deleteExpression) { delete(object, deleteExpression, this.config); } /** * Deletes the given object from its DynamoDB table using the specified configuration. */ public void delete(Object object, DynamoDBMapperConfig config) { delete(object, null, config); } /** * Deletes the given object from its DynamoDB table using the provided deleteExpression and provided configuration. * Any options specified in the deleteExpression parameter will be overlaid on any constraints due to * versioned attributes. * @param deleteExpression * The options to apply to this delete request * @param config * Config override object. If {@link SaveBehavior#CLOBBER} is * supplied, version fields will not be considered when deleting * the object. */ public <T> void delete(T object, DynamoDBDeleteExpression deleteExpression, DynamoDBMapperConfig config) { config = mergeConfig(config); @SuppressWarnings("unchecked") Class<T> clazz = (Class<T>) object.getClass(); String tableName = getTableName(clazz, config); Map<String, AttributeValue> key = getKey(object, clazz); /* * 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<String, ExpectedAttributeValue> internalAssertions = new HashMap<String, ExpectedAttributeValue>(); 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); internalAssertions.put(attributeName, expected); break; } } } // Overlay any user provided expected values onto the generated ones Map<String, ExpectedAttributeValue> expectedValues = internalAssertions; String conditionOperator = null; if (deleteExpression != null) { expectedValues = mergeExpectedAttributeValueConditions(internalAssertions, deleteExpression.getExpected(), deleteExpression.getConditionalOperator()); conditionOperator = deleteExpression.getConditionalOperator(); } DeleteItemRequest req = applyUserAgent( new DeleteItemRequest().withKey(key).withTableName(tableName).withExpected(expectedValues)) .withConditionalOperator(conditionOperator) .withRequestMetricCollector(config.getRequestMetricCollector()); db.deleteItem(req); } /** * Deletes the objects given using one or more calls to the * {@link AmazonDynamoDB#batchWriteItem(BatchWriteItemRequest)} API. <b>No * version checks are performed</b>, as required by the API. * * @see DynamoDBMapper#batchWrite(List, List, DynamoDBMapperConfig) */ public List<FailedBatch> batchDelete(List<? extends Object> objectsToDelete) { return batchWrite(Collections.emptyList(), objectsToDelete, this.config); } /** * Deletes the objects given using one or more calls to the * {@link AmazonDynamoDB#batchWriteItem(BatchWriteItemRequest)} API. <b>No * version checks are performed</b>, as required by the API. * * @see DynamoDBMapper#batchWrite(List, List, DynamoDBMapperConfig) */ public List<FailedBatch> batchDelete(Object... objectsToDelete) { return batchWrite(Collections.emptyList(), Arrays.asList(objectsToDelete), this.config); } /** * Saves the objects given using one or more calls to the * {@link AmazonDynamoDB#batchWriteItem(BatchWriteItemRequest)} API. <b>No * version checks are performed</b>, as required by the API. * <p/> * <b>This method ignores any SaveBehavior set on the mapper</b>, and * always behaves as if SaveBehavior.CLOBBER was specified, as * the AmazonDynamoDB.batchWriteItem() request does not support updating * existing items. * * @see DynamoDBMapper#batchWrite(List, List, DynamoDBMapperConfig) */ public List<FailedBatch> batchSave(List<? extends Object> objectsToSave) { return batchWrite(objectsToSave, Collections.emptyList(), this.config); } /** * Saves the objects given using one or more calls to the * {@link AmazonDynamoDB#batchWriteItem(BatchWriteItemRequest)} API. <b>No * version checks are performed</b>, as required by the API. * <p/> * <b>This method ignores any SaveBehavior set on the mapper</b>, and * always behaves as if SaveBehavior.CLOBBER was specified, as * the AmazonDynamoDB.batchWriteItem() request does not support updating * existing items. * * @see DynamoDBMapper#batchWrite(List, List, DynamoDBMapperConfig) */ public List<FailedBatch> batchSave(Object... objectsToSave) { return 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. <b>No * version checks are performed</b>, as required by the API. * <p/> * <b>This method ignores any SaveBehavior set on the mapper</b>, and * always behaves as if SaveBehavior.CLOBBER was specified, as * the AmazonDynamoDB.batchWriteItem() request does not support updating * existing items. * * @see DynamoDBMapper#batchWrite(List, List, DynamoDBMapperConfig) */ public List<FailedBatch> batchWrite(List<? extends Object> objectsToWrite, List<? extends Object> objectsToDelete) { return 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. <b>No version checks * are performed</b>, as required by the * {@link AmazonDynamoDB#batchWriteItem(BatchWriteItemRequest)} * API. * @param objectsToDelete * A list of objects to delete from DynamoDB. <b>No version * checks are performed</b>, 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. In particular, this method <b>always acts as * if SaveBehavior.CLOBBER was specified</b> regardless of the * value of the config parameter. * @return A list of failed batches which includes the unprocessed items and * the exceptions causing the failure. */ public List<FailedBatch> batchWrite(List<? extends Object> objectsToWrite, List<? extends Object> objectsToDelete, DynamoDBMapperConfig config) { config = mergeConfig(config); List<FailedBatch> totalFailedBatches = new LinkedList<FailedBatch>(); HashMap<String, List<WriteRequest>> requestItems = new HashMap<String, List<WriteRequest>>(); List<ValueUpdate> inMemoryUpdates = new LinkedList<ValueUpdate>(); for (Object toWrite : objectsToWrite) { Class<?> clazz = toWrite.getClass(); String tableName = getTableName(clazz, config); Map<String, AttributeValue> attributeValues = new HashMap<String, AttributeValue>(); // 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<WriteRequest>()); } AttributeTransformer.Parameters<?> parameters = toParameters(attributeValues, clazz, config); requestItems.get(tableName).add( new WriteRequest().withPutRequest(new PutRequest().withItem(transformAttributes(parameters)))); } for (Object toDelete : objectsToDelete) { Class<?> clazz = toDelete.getClass(); String tableName = getTableName(clazz, config); Map<String, AttributeValue> key = getKey(toDelete); if (!requestItems.containsKey(tableName)) { requestItems.put(tableName, new LinkedList<WriteRequest>()); } requestItems.get(tableName).add(new WriteRequest().withDeleteRequest(new DeleteRequest().withKey(key))); } // Break into chunks of 25 items and make service requests to DynamoDB while (!requestItems.isEmpty()) { HashMap<String, List<WriteRequest>> batch = new HashMap<String, List<WriteRequest>>(); int i = 0; Iterator<Entry<String, List<WriteRequest>>> tableIter = requestItems.entrySet().iterator(); while (tableIter.hasNext() && i < 25) { Entry<String, List<WriteRequest>> tableRequest = tableIter.next(); batch.put(tableRequest.getKey(), new LinkedList<WriteRequest>()); Iterator<WriteRequest> 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(); } } List<FailedBatch> failedBatches = writeOneBatch(batch); if (failedBatches != null) { totalFailedBatches.addAll(failedBatches); // If contains throttling exception, we do a backoff if (containsThrottlingException(failedBatches)) { try { Thread.sleep(1000 * 2); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new AmazonClientException(e.getMessage(), e); } } } } // Once the entire batch is processed, update assigned keys in memory for (ValueUpdate update : inMemoryUpdates) { update.apply(); } return totalFailedBatches; } /** * Process one batch of requests(max 25). It will divide the batch if * receives request too large exception(the total size of the request is beyond 1M). */ private List<FailedBatch> writeOneBatch(Map<String, List<WriteRequest>> batch) { List<FailedBatch> failedBatches = new LinkedList<FailedBatch>(); Map<String, List<WriteRequest>> firstHalfBatch = new HashMap<String, List<WriteRequest>>(); Map<String, List<WriteRequest>> secondHalfBatch = new HashMap<String, List<WriteRequest>>(); FailedBatch failedBatch = callUntilCompletion(batch); if (failedBatch != null) { // If the exception is request entity too large, we divide the batch // into smaller parts. if (failedBatch.getException() instanceof AmazonServiceException && RetryUtils .isRequestEntityTooLargeException((AmazonServiceException) failedBatch.getException())) { // If only one item left, the item size must beyond 64k, which // exceedes the limit. if (computeFailedBatchSize(failedBatch) == 1) { failedBatches.add(failedBatch); } else { divideBatch(batch, firstHalfBatch, secondHalfBatch); failedBatches.addAll(writeOneBatch(firstHalfBatch)); failedBatches.addAll(writeOneBatch(secondHalfBatch)); } } else { failedBatches.add(failedBatch); } } return failedBatches; } /** * Check whether there are throttling exception in the failed batches. */ private boolean containsThrottlingException(List<FailedBatch> failedBatches) { for (FailedBatch failedBatch : failedBatches) { Exception e = failedBatch.getException(); if (e instanceof AmazonServiceException && RetryUtils.isThrottlingException((AmazonServiceException) e)) { return true; } } return false; } /** * Divide the batch of objects to save into two smaller batches. Each contains half of the elements. */ private void divideBatch(Map<String, List<WriteRequest>> batch, Map<String, List<WriteRequest>> firstHalfBatch, Map<String, List<WriteRequest>> secondHalfBatch) { for (String key : batch.keySet()) { List<WriteRequest> requests = batch.get(key); List<WriteRequest> firstHalfRequests = requests.subList(0, requests.size() / 2); List<WriteRequest> secondHalfRequests = requests.subList(requests.size() / 2, requests.size()); firstHalfBatch.put(key, firstHalfRequests); secondHalfBatch.put(key, secondHalfRequests); } } /** * Count the total number of unprocessed items in the failed batch. */ private int computeFailedBatchSize(FailedBatch failedBatch) { int count = 0; for (String tableName : failedBatch.getUnprocessedItems().keySet()) { count += failedBatch.getUnprocessedItems().get(tableName).size(); } return count; } /** * Continue trying to process the batch until it finishes or an exception * occurs. */ private FailedBatch callUntilCompletion(Map<String, List<WriteRequest>> batch) { BatchWriteItemResult result = null; int retries = 0; FailedBatch failedBatch = null; while (true) { try { result = db.batchWriteItem(new BatchWriteItemRequest().withRequestItems(batch)); } catch (Exception e) { failedBatch = new FailedBatch(); failedBatch.setUnprocessedItems(batch); failedBatch.setException(e); return failedBatch; } retries++; batch = result.getUnprocessedItems(); if (batch.size() > 0) { pauseExponentially(retries); } else { break; } } return failedBatch; } /** * Retrieves multiple items from multiple tables using their primary keys. * * @see DynamoDBMapper#batchLoad(List, DynamoDBMapperConfig) * * @return A map of the loaded objects. Each key in the map is the name of a * DynamoDB table. Each value in the map is a list of objects that * have been loaded from that table. All objects for each table can * be cast to the associated user defined type that is annotated as * mapping that table. */ public Map<String, List<Object>> batchLoad(List<Object> itemsToGet) { return batchLoad(itemsToGet, this.config); } /** * Retrieves multiple items from multiple tables using their primary keys. * * @param itemsToGet * Key objects, corresponding to the class to fetch, with their * primary key values set. * @param config * Only {@link DynamoDBMapperConfig#getTableNameOverride()} and * {@link DynamoDBMapperConfig#getConsistentReads()} are * considered. * * @return A map of the loaded objects. Each key in the map is the name of a * DynamoDB table. Each value in the map is a list of objects that * have been loaded from that table. All objects for each table can * be cast to the associated user defined type that is annotated as * mapping that table. */ public Map<String, List<Object>> batchLoad(List<Object> itemsToGet, DynamoDBMapperConfig config) { config = mergeConfig(config); boolean consistentReads = (config.getConsistentReads() == ConsistentReads.CONSISTENT); if (itemsToGet == null || itemsToGet.isEmpty()) { return new HashMap<String, List<Object>>(); } Map<String, KeysAndAttributes> requestItems = new HashMap<String, KeysAndAttributes>(); Map<String, Class<?>> classesByTableName = new HashMap<String, Class<?>>(); Map<String, List<Object>> resultSet = new HashMap<String, List<Object>>(); int count = 0; for (Object keyObject : itemsToGet) { Class<?> clazz = keyObject.getClass(); String tableName = getTableName(clazz, config); classesByTableName.put(tableName, clazz); if (!requestItems.containsKey(tableName)) { requestItems.put(tableName, new KeysAndAttributes().withConsistentRead(consistentReads) .withKeys(new LinkedList<Map<String, AttributeValue>>())); } requestItems.get(tableName).getKeys().add(getKey(keyObject)); // Reach the maximum number which can be handled in a single batchGet if (++count == 100) { processBatchGetRequest(classesByTableName, requestItems, resultSet, config); requestItems.clear(); count = 0; } } if (count > 0) { processBatchGetRequest(classesByTableName, requestItems, resultSet, config); } return resultSet; } /** * Retrieves the attributes for multiple items from multiple tables using * their primary keys. * {@link AmazonDynamoDB#batchGetItem(BatchGetItemRequest)} API. * * @return A map of the loaded objects. Each key in the map is the name of a * DynamoDB table. Each value in the map is a list of objects that * have been loaded from that table. All objects for each table can * be cast to the associated user defined type that is annotated as * mapping that table. * * @see #batchLoad(List, DynamoDBMapperConfig) * @see #batchLoad(Map, DynamoDBMapperConfig) */ public Map<String, List<Object>> batchLoad(Map<Class<?>, List<KeyPair>> itemsToGet) { return batchLoad(itemsToGet, this.config); } /** * Retrieves multiple items from multiple tables using their primary keys. * Valid only for tables with a single hash key, or a single hash and range * key. For other schemas, use * {@link DynamoDBMapper#batchLoad(List, DynamoDBMapperConfig)} * * @param itemsToGet * Map from class to load to list of primary key attributes. * @param config * Only {@link DynamoDBMapperConfig#getTableNameOverride()} and * {@link DynamoDBMapperConfig#getConsistentReads()} are * considered. * * @return A map of the loaded objects. Each key in the map is the name of a * DynamoDB table. Each value in the map is a list of objects that * have been loaded from that table. All objects for each table can * be cast to the associated user defined type that is annotated as * mapping that table. */ public Map<String, List<Object>> batchLoad(Map<Class<?>, List<KeyPair>> itemsToGet, DynamoDBMapperConfig config) { List<Object> keys = new ArrayList<Object>(); if (itemsToGet != null) { for (Class<?> clazz : itemsToGet.keySet()) { if (itemsToGet.get(clazz) != null) { for (KeyPair keyPair : itemsToGet.get(clazz)) { keys.add(createKeyObject(clazz, keyPair.getHashKey(), keyPair.getRangeKey())); } } } } return batchLoad(keys, config); } /** * @param config never null */ private void processBatchGetRequest(final Map<String, Class<?>> classesByTableName, final Map<String, KeysAndAttributes> requestItems, final Map<String, List<Object>> resultSet, final DynamoDBMapperConfig config) { BatchGetItemResult batchGetItemResult = null; BatchGetItemRequest batchGetItemRequest = new BatchGetItemRequest() .withRequestMetricCollector(config.getRequestMetricCollector()); batchGetItemRequest.setRequestItems(requestItems); int retries = 0; int noOfItemsInOriginalRequest = requestItems.size(); do { if (batchGetItemResult != null) { retries++; if (noOfItemsInOriginalRequest == batchGetItemResult.getUnprocessedKeys().size()) { pauseExponentially(retries); if (retries > BATCH_GET_MAX_RETRY_COUNT_ALL_KEYS) { throw new AmazonClientException( "Batch Get Item request to server hasn't received any data. Please try again later."); } } batchGetItemRequest.setRequestItems(batchGetItemResult.getUnprocessedKeys()); } batchGetItemResult = db.batchGetItem(batchGetItemRequest); Map<String, List<Map<String, AttributeValue>>> responses = batchGetItemResult.getResponses(); for (String tableName : responses.keySet()) { List<Object> objects = null; if (resultSet.get(tableName) != null) { objects = resultSet.get(tableName); } else { objects = new LinkedList<Object>(); } Class<?> clazz = classesByTableName.get(tableName); for (Map<String, AttributeValue> item : responses.get(tableName)) { AttributeTransformer.Parameters<?> parameters = toParameters(item, clazz, config); objects.add(marshalIntoObject(parameters)); } resultSet.put(tableName, objects); } // To see whether there are unprocessed keys. } while (batchGetItemResult.getUnprocessedKeys() != null && batchGetItemResult.getUnprocessedKeys().size() > 0); } /** * 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<String, AttributeValue> convertToItem(Map<String, AttributeValueUpdate> putValues) { Map<String, AttributeValue> map = new HashMap<String, AttributeValue>(); for (Entry<String, AttributeValueUpdate> entry : putValues.entrySet()) { String attributeName = entry.getKey(); AttributeValue attributeValue = entry.getValue().getValue(); String attributeAction = entry.getValue().getAction(); /* * AttributeValueUpdate allows nulls for its values, since they are * semantically meaningful. AttributeValues never have null values. */ if (attributeValue != null && !AttributeAction.DELETE.toString().equals(attributeAction)) { map.put(attributeName, attributeValue); } } 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 <T> PaginatedScanList<T> scan(Class<T> 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. * <p> * Callers should be aware that the returned list is unmodifiable, and any * attempts to modify the list will result in an * UnsupportedOperationException. * <p> * You can specify the pagination loading strategy for this scan operation. * By default, the list returned is lazily loaded when possible. * * @param <T> * 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 * @see PaginationLoadingStrategy */ public <T> PaginatedScanList<T> scan(Class<T> clazz, DynamoDBScanExpression scanExpression, DynamoDBMapperConfig config) { config = mergeConfig(config); ScanRequest scanRequest = createScanRequestFromExpression(clazz, scanExpression, config); ScanResult scanResult = db.scan(applyUserAgent(scanRequest)); return new PaginatedScanList<T>(this, clazz, db, scanRequest, scanResult, config.getPaginationLoadingStrategy(), config); } /** * Scans through an Amazon DynamoDB table on logically partitioned segments * in parallel and returns the matching results in one unmodifiable list of * instantiated objects, using the default configuration. * * @see DynamoDBMapper#parallelScan(Class, DynamoDBScanExpression,int, * DynamoDBMapperConfig) */ public <T> PaginatedParallelScanList<T> parallelScan(Class<T> clazz, DynamoDBScanExpression scanExpression, int totalSegments) { return parallelScan(clazz, scanExpression, totalSegments, config); } /** * Scans through an Amazon DynamoDB table on logically partitioned segments * in parallel. This method will create a thread pool of the specified size, * and each thread will issue scan requests for its assigned segment, * following the returned continuation token, until the end of its segment. * Callers should be responsible for setting the appropriate number of total * segments. More scan segments would result in better performance but more * consumed capacity of the table. The results are returned in one * 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. * <p> * Callers should be aware that the returned list is unmodifiable, and any * attempts to modify the list will result in an * UnsupportedOperationException. * <p> * You can specify the pagination loading strategy for this parallel scan operation. * By default, the list returned is lazily loaded when possible. * * @param <T> * 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 totalSegments * Number of total parallel scan segments. * <b>Range: </b>1 - 4096 * @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 PaginatedParallelScanList * @see PaginationLoadingStrategy */ public <T> PaginatedParallelScanList<T> parallelScan(Class<T> clazz, DynamoDBScanExpression scanExpression, int totalSegments, DynamoDBMapperConfig config) { config = mergeConfig(config); // Create hard copies of the original scan request with difference segment number. List<ScanRequest> parallelScanRequests = createParallelScanRequestsFromExpression(clazz, scanExpression, totalSegments, config); ParallelScanTask parallelScanTask = new ParallelScanTask(this, db, parallelScanRequests); return new PaginatedParallelScanList<T>(this, clazz, db, parallelScanTask, config.getPaginationLoadingStrategy(), config); } /** * 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 <T> * 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 <T> ScanResultPage<T> scanPage(Class<T> clazz, DynamoDBScanExpression scanExpression, DynamoDBMapperConfig config) { config = mergeConfig(config); ScanRequest scanRequest = createScanRequestFromExpression(clazz, scanExpression, config); ScanResult scanResult = db.scan(applyUserAgent(scanRequest)); ScanResultPage<T> result = new ScanResultPage<T>(); List<AttributeTransformer.Parameters<T>> parameters = toParameters(scanResult.getItems(), clazz, config); result.setResults(marshalIntoObjects(parameters)); 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 <T> ScanResultPage<T> scanPage(Class<T> 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 <T> PaginatedQueryList<T> query(Class<T> clazz, DynamoDBQueryExpression<T> 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. * <p> * When the query is on any local/global secondary index, callers should be aware that * the returned object(s) will only contain item attributes that are projected * into the index. All the other unprojected attributes will be saved as type * default values. * <p> * Callers should also be aware that the returned list is unmodifiable, and any * attempts to modify the list will result in an * UnsupportedOperationException. * <p> * You can specify the pagination loading strategy for this query operation. * By default, the list returned is lazily loaded when possible. * * @param <T> * 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 * @see PaginationLoadingStrategy */ public <T> PaginatedQueryList<T> query(Class<T> clazz, DynamoDBQueryExpression<T> queryExpression, DynamoDBMapperConfig config) { config = mergeConfig(config); QueryRequest queryRequest = createQueryRequestFromExpression(clazz, queryExpression, config); QueryResult queryResult = db.query(applyUserAgent(queryRequest)); return new PaginatedQueryList<T>(this, clazz, db, queryRequest, queryResult, config.getPaginationLoadingStrategy(), 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. * * @see DynamoDBMapper#queryPage(Class, DynamoDBQueryExpression, DynamoDBMapperConfig) */ public <T> QueryResultPage<T> queryPage(Class<T> clazz, DynamoDBQueryExpression<T> 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 <T> * 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 <T> QueryResultPage<T> queryPage(Class<T> clazz, DynamoDBQueryExpression<T> queryExpression, DynamoDBMapperConfig config) { config = mergeConfig(config); QueryRequest queryRequest = createQueryRequestFromExpression(clazz, queryExpression, config); QueryResult scanResult = db.query(applyUserAgent(queryRequest)); QueryResultPage<T> result = new QueryResultPage<T>(); List<AttributeTransformer.Parameters<T>> parameters = toParameters(scanResult.getItems(), clazz, config); result.setResults(marshalIntoObjects(parameters)); 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. * <p> * 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.setSelect(Select.COUNT); // 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 <T> int count(Class<T> clazz, DynamoDBQueryExpression<T> 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 <T> int count(Class<T> clazz, DynamoDBQueryExpression<T> queryExpression, DynamoDBMapperConfig config) { config = mergeConfig(config); QueryRequest queryRequest = createQueryRequestFromExpression(clazz, queryExpression, config); queryRequest.setSelect(Select.COUNT); // 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; } /** * @param config never null */ 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()); scanRequest.setTotalSegments(scanExpression.getTotalSegments()); scanRequest.setSegment(scanExpression.getSegment()); scanRequest.setConditionalOperator(scanExpression.getConditionalOperator()); scanRequest.setRequestMetricCollector(config.getRequestMetricCollector()); return scanRequest; } /** * @param config never null */ private List<ScanRequest> createParallelScanRequestsFromExpression(Class<?> clazz, DynamoDBScanExpression scanExpression, int totalSegments, DynamoDBMapperConfig config) { if (totalSegments < 1) { throw new IllegalArgumentException("Parallel scan should have at least one scan segment."); } if (scanExpression.getExclusiveStartKey() != null) { log.info("The ExclusiveStartKey parameter specified in the DynamoDBScanExpression is ignored," + " since the individual parallel scan request on each segment is applied on a separate key scope."); } if (scanExpression.getSegment() != null || scanExpression.getTotalSegments() != null) { log.info( "The Segment and TotalSegments parameters specified in the DynamoDBScanExpression are ignored."); } List<ScanRequest> parallelScanRequests = new LinkedList<ScanRequest>(); for (int segment = 0; segment < totalSegments; segment++) { ScanRequest scanRequest = createScanRequestFromExpression(clazz, scanExpression, config); parallelScanRequests.add( scanRequest.withSegment(segment).withTotalSegments(totalSegments).withExclusiveStartKey(null)); } return parallelScanRequests; } private <T> QueryRequest createQueryRequestFromExpression(Class<T> clazz, DynamoDBQueryExpression<T> queryExpression, DynamoDBMapperConfig config) { QueryRequest queryRequest = new QueryRequest(); queryRequest.setConsistentRead(queryExpression.isConsistentRead()); queryRequest.setTableName(getTableName(clazz, config)); queryRequest.setIndexName(queryExpression.getIndexName()); // Hash key (primary or index) conditions Map<String, Condition> hashKeyConditions = getHashKeyEqualsConditions(queryExpression.getHashKeyValues()); // Range key (primary or index) conditions Map<String, Condition> rangeKeyConditions = queryExpression.getRangeKeyConditions(); processKeyConditions(clazz, queryRequest, hashKeyConditions, rangeKeyConditions); queryRequest.setScanIndexForward(queryExpression.isScanIndexForward()); queryRequest.setLimit(queryExpression.getLimit()); queryRequest.setExclusiveStartKey(queryExpression.getExclusiveStartKey()); queryRequest.setQueryFilter(queryExpression.getQueryFilter()); queryRequest.setConditionalOperator(queryExpression.getConditionalOperator()); queryRequest.setRequestMetricCollector(config.getRequestMetricCollector()); return queryRequest; } /** * Utility method for checking the validity of both hash and range key * conditions. It also tries to infer the correct index name from the POJO * annotation, if such information is not directly specified by the user. * * @param clazz * The domain class of the queried items. * @param queryRequest * The QueryRequest object to be sent to service. * @param hashKeyConditions * All the hash key EQ conditions extracted from the POJO object. * The mapper will choose one of them that could be applied together with * the user-specified (if any) index name and range key conditions. Or it * throws error if more than one conditions are applicable for the query. * @param rangeKeyConditions * The range conditions specified by the user. We currently only * allow at most one range key condition. */ private void processKeyConditions(Class<?> clazz, QueryRequest queryRequest, Map<String, Condition> hashKeyConditions, Map<String, Condition> rangeKeyConditions) { // There should be least one hash key condition. if (hashKeyConditions == null || hashKeyConditions.isEmpty()) { throw new IllegalArgumentException( "Illegal query expression: No hash key condition is found in the query"); } // We don't allow multiple range key conditions. if (rangeKeyConditions != null && rangeKeyConditions.size() > 1) { throw new IllegalArgumentException("Illegal query expression: Conditions on multiple range keys (" + rangeKeyConditions.keySet().toString() + ") are found in the query. DynamoDB service only accepts up to ONE range key condition."); } final boolean hasRangeKeyCondition = (rangeKeyConditions != null) && (!rangeKeyConditions.isEmpty()); final String userProvidedIndexName = queryRequest.getIndexName(); final String primaryHashKeyName = reflector.getPrimaryHashKeyName(clazz); final TableIndexesInfo parsedIndexesInfo = schemaParser.parseTableIndexes(clazz, reflector); // First collect the names of all the global/local secondary indexes that could be applied to this query. // If the user explicitly specified an index name, we also need to // 1) check the index is applicable for both hash and range key conditions // 2) choose one hash key condition if there are more than one of them boolean hasPrimaryHashKeyCondition = false; final Map<String, Set<String>> annotatedGSIsOnHashKeys = new HashMap<String, Set<String>>(); String hashKeyNameForThisQuery = null; boolean hasPrimaryRangeKeyCondition = false; final Set<String> annotatedLSIsOnRangeKey = new HashSet<String>(); final Set<String> annotatedGSIsOnRangeKey = new HashSet<String>(); // Range key condition String rangeKeyNameForThisQuery = null; if (hasRangeKeyCondition) { for (String rangeKeyName : rangeKeyConditions.keySet()) { rangeKeyNameForThisQuery = rangeKeyName; if (reflector.hasPrimaryRangeKey(clazz) && rangeKeyName.equals(reflector.getPrimaryRangeKeyName(clazz))) { hasPrimaryRangeKeyCondition = true; } Collection<String> annotatedLSI = parsedIndexesInfo.getLsiNamesByIndexRangeKey(rangeKeyName); if (annotatedLSI != null) { annotatedLSIsOnRangeKey.addAll(annotatedLSI); } Collection<String> annotatedGSI = parsedIndexesInfo.getGsiNamesByIndexRangeKey(rangeKeyName); if (annotatedGSI != null) { annotatedGSIsOnRangeKey.addAll(annotatedGSI); } } if (!hasPrimaryRangeKeyCondition && annotatedLSIsOnRangeKey.isEmpty() && annotatedGSIsOnRangeKey.isEmpty()) { throw new DynamoDBMappingException( "The query contains a condition on a range key (" + rangeKeyNameForThisQuery + ") " + "that is not annotated with either @DynamoDBRangeKey or @DynamoDBIndexRangeKey."); } } final boolean userProvidedLSIWithRangeKeyCondition = (userProvidedIndexName != null) && (annotatedLSIsOnRangeKey.contains(userProvidedIndexName)); final boolean hashOnlyLSIQuery = (userProvidedIndexName != null) && (!hasRangeKeyCondition) && parsedIndexesInfo.getAllLsiNames().contains(userProvidedIndexName); final boolean userProvidedLSI = userProvidedLSIWithRangeKeyCondition || hashOnlyLSIQuery; final boolean userProvidedGSIWithRangeKeyCondition = (userProvidedIndexName != null) && (annotatedGSIsOnRangeKey.contains(userProvidedIndexName)); final boolean hashOnlyGSIQuery = (userProvidedIndexName != null) && (!hasRangeKeyCondition) && parsedIndexesInfo.getAllGsiNames().contains(userProvidedIndexName); final boolean userProvidedGSI = userProvidedGSIWithRangeKeyCondition || hashOnlyGSIQuery; if (userProvidedLSI && userProvidedGSI) { throw new DynamoDBMappingException("Invalid query: " + "Index \"" + userProvidedIndexName + "\" " + "is annotateded as both a LSI and a GSI for attribute."); } // Hash key conditions for (String hashKeyName : hashKeyConditions.keySet()) { if (hashKeyName.equals(primaryHashKeyName)) { hasPrimaryHashKeyCondition = true; } Collection<String> annotatedGSINames = parsedIndexesInfo.getGsiNamesByIndexHashKey(hashKeyName); annotatedGSIsOnHashKeys.put(hashKeyName, annotatedGSINames == null ? new HashSet<String>() : new HashSet<String>(annotatedGSINames)); // Additional validation if the user provided an index name. if (userProvidedIndexName != null) { boolean foundHashKeyConditionValidWithUserProvidedIndex = false; if (userProvidedLSI && hashKeyName.equals(primaryHashKeyName)) { // found an applicable hash key condition (primary hash + LSI range) foundHashKeyConditionValidWithUserProvidedIndex = true; } else if (userProvidedGSI && annotatedGSINames != null && annotatedGSINames.contains(userProvidedIndexName)) { // found an applicable hash key condition (GSI hash + range) foundHashKeyConditionValidWithUserProvidedIndex = true; } if (foundHashKeyConditionValidWithUserProvidedIndex) { if (hashKeyNameForThisQuery != null) { throw new IllegalArgumentException( "Ambiguous query expression: More than one hash key EQ conditions (" + hashKeyNameForThisQuery + ", " + hashKeyName + ") are applicable to the specified index (" + userProvidedIndexName + "). " + "Please provide only one of them in the query expression."); } else { // found an applicable hash key condition hashKeyNameForThisQuery = hashKeyName; } } } } // Collate all the key conditions Map<String, Condition> keyConditions = new HashMap<String, Condition>(); // With user-provided index name if (userProvidedIndexName != null) { if (hasRangeKeyCondition && (!userProvidedLSI) && (!userProvidedGSI)) { throw new IllegalArgumentException( "Illegal query expression: No range key condition is applicable to the specified index (" + userProvidedIndexName + "). "); } if (hashKeyNameForThisQuery == null) { throw new IllegalArgumentException( "Illegal query expression: No hash key condition is applicable to the specified index (" + userProvidedIndexName + "). "); } keyConditions.put(hashKeyNameForThisQuery, hashKeyConditions.get(hashKeyNameForThisQuery)); if (hasRangeKeyCondition) { keyConditions.putAll(rangeKeyConditions); } } // Infer the index name by finding the index shared by both hash and range key annotations. else { if (hasRangeKeyCondition) { String inferredIndexName = null; hashKeyNameForThisQuery = null; if (hasPrimaryHashKeyCondition && hasPrimaryRangeKeyCondition) { // Found valid query: primary hash + range key conditions hashKeyNameForThisQuery = primaryHashKeyName; } else { // Intersect the set of all the indexes applicable to the range key // with the set of indexes applicable to each hash key condition. for (String hashKeyName : annotatedGSIsOnHashKeys.keySet()) { boolean foundValidQueryExpressionWithInferredIndex = false; String indexNameInferredByThisHashKey = null; if (hashKeyName.equals(primaryHashKeyName)) { if (annotatedLSIsOnRangeKey.size() == 1) { // Found valid query (Primary hash + LSI range conditions) foundValidQueryExpressionWithInferredIndex = true; indexNameInferredByThisHashKey = annotatedLSIsOnRangeKey.iterator().next(); } } Set<String> annotatedGSIsOnHashKey = annotatedGSIsOnHashKeys.get(hashKeyName); // We don't need the data in annotatedGSIsOnHashKeys afterwards, // so it's safe to do the intersection in-place. annotatedGSIsOnHashKey.retainAll(annotatedGSIsOnRangeKey); if (annotatedGSIsOnHashKey.size() == 1) { // Found valid query (Hash + range conditions on a GSI) if (foundValidQueryExpressionWithInferredIndex) { hashKeyNameForThisQuery = hashKeyName; inferredIndexName = indexNameInferredByThisHashKey; } foundValidQueryExpressionWithInferredIndex = true; indexNameInferredByThisHashKey = annotatedGSIsOnHashKey.iterator().next(); } if (foundValidQueryExpressionWithInferredIndex) { if (hashKeyNameForThisQuery != null) { throw new IllegalArgumentException( "Ambiguous query expression: Found multiple valid queries: " + "(Hash: \"" + hashKeyNameForThisQuery + "\", Range: \"" + rangeKeyNameForThisQuery + "\", Index: \"" + inferredIndexName + "\") and " + "(Hash: \"" + hashKeyName + "\", Range: \"" + rangeKeyNameForThisQuery + "\", Index: \"" + indexNameInferredByThisHashKey + "\")."); } else { hashKeyNameForThisQuery = hashKeyName; inferredIndexName = indexNameInferredByThisHashKey; } } } } if (hashKeyNameForThisQuery != null) { keyConditions.put(hashKeyNameForThisQuery, hashKeyConditions.get(hashKeyNameForThisQuery)); keyConditions.putAll(rangeKeyConditions); queryRequest.setIndexName(inferredIndexName); } else { throw new IllegalArgumentException( "Illegal query expression: Cannot infer the index name from the query expression."); } } else { // No range key condition is specified. if (hashKeyConditions.size() > 1) { if (hasPrimaryHashKeyCondition) { keyConditions.put(primaryHashKeyName, hashKeyConditions.get(primaryHashKeyName)); } else { throw new IllegalArgumentException( "Ambiguous query expression: More than one index hash key EQ conditions (" + hashKeyConditions.keySet() + ") are applicable to the query. " + "Please provide only one of them in the query expression, or specify the appropriate index name."); } } else { // Only one hash key condition String hashKeyName = annotatedGSIsOnHashKeys.keySet().iterator().next(); if (!hasPrimaryHashKeyCondition) { if (annotatedGSIsOnHashKeys.get(hashKeyName).size() == 1) { // Set the index if the index hash key is only annotated with one GSI. queryRequest.setIndexName(annotatedGSIsOnHashKeys.get(hashKeyName).iterator().next()); } else if (annotatedGSIsOnHashKeys.get(hashKeyName).size() > 1) { throw new IllegalArgumentException("Ambiguous query expression: More than one GSIs (" + annotatedGSIsOnHashKeys.get(hashKeyName) + ") are applicable to the query. " + "Please specify one of them in your query expression."); } else { throw new IllegalArgumentException( "Illegal query expression: No GSI is found in the @DynamoDBIndexHashKey annotation for attribute " + "\"" + hashKeyName + "\"."); } } keyConditions.putAll(hashKeyConditions); } } } queryRequest.setKeyConditions(keyConditions); } private <T> AttributeTransformer.Parameters<T> toParameters(final Map<String, AttributeValue> attributeValues, final Class<T> modelClass, final DynamoDBMapperConfig mapperConfig) { return toParameters(attributeValues, false, modelClass, mapperConfig); } private <T> AttributeTransformer.Parameters<T> toParameters(final Map<String, AttributeValue> attributeValues, final boolean partialUpdate, final Class<T> modelClass, final DynamoDBMapperConfig mapperConfig) { return new TransformerParameters(reflector, attributeValues, partialUpdate, modelClass, mapperConfig); } final <T> List<AttributeTransformer.Parameters<T>> toParameters( final List<Map<String, AttributeValue>> attributeValues, final Class<T> modelClass, final DynamoDBMapperConfig mapperConfig) { List<AttributeTransformer.Parameters<T>> rval = new ArrayList<AttributeTransformer.Parameters<T>>( attributeValues.size()); for (Map<String, AttributeValue> item : attributeValues) { rval.add(toParameters(item, modelClass, mapperConfig)); } return rval; } /** * The one true implementation of AttributeTransformer.Parameters. */ private static class TransformerParameters<T> implements AttributeTransformer.Parameters<T> { private final DynamoDBReflector reflector; private final Map<String, AttributeValue> attributeValues; private final boolean partialUpdate; private final Class<T> modelClass; private final DynamoDBMapperConfig mapperConfig; private String tableName; private String hashKeyName; private String rangeKeyName; public TransformerParameters(final DynamoDBReflector reflector, final Map<String, AttributeValue> attributeValues, final boolean partialUpdate, final Class<T> modelClass, final DynamoDBMapperConfig mapperConfig) { this.reflector = reflector; this.attributeValues = Collections.unmodifiableMap(attributeValues); this.partialUpdate = partialUpdate; this.modelClass = modelClass; this.mapperConfig = mapperConfig; } @Override public Map<String, AttributeValue> getAttributeValues() { return attributeValues; } @Override public boolean isPartialUpdate() { return partialUpdate; } @Override public Class<T> getModelClass() { return modelClass; } @Override public DynamoDBMapperConfig getMapperConfig() { return mapperConfig; } @Override public String getTableName() { if (tableName == null) { tableName = DynamoDBMapper.getTableName(modelClass, mapperConfig, reflector); } return tableName; } @Override public String getHashKeyName() { if (hashKeyName == null) { Method hashKeyGetter = reflector.getPrimaryHashKeyGetter(modelClass); hashKeyName = reflector.getAttributeName(hashKeyGetter); } return hashKeyName; } @Override public String getRangeKeyName() { if (rangeKeyName == null) { Method rangeKeyGetter = reflector.getPrimaryRangeKeyGetter(modelClass); if (rangeKeyGetter == null) { rangeKeyName = NO_RANGE_KEY; } else { rangeKeyName = reflector.getAttributeName(rangeKeyGetter); } } if (rangeKeyName == NO_RANGE_KEY) { return null; } return rangeKeyName; } } private Map<String, AttributeValue> untransformAttributes(final AttributeTransformer.Parameters parameters) { if (transformer != null) { return transformer.untransform(parameters); } return untransformAttributes(parameters.getModelClass(), parameters.getAttributeValues()); } /** * By default, just calls {@link #untransformAttributes(String, String, Map)}. * * @deprecated in favor of {@link AttributeTransformer} */ @Deprecated protected Map<String, AttributeValue> untransformAttributes(Class<?> clazz, Map<String, AttributeValue> attributeValues) { Method hashKeyGetter = reflector.getPrimaryHashKeyGetter(clazz); String hashKeyName = reflector.getAttributeName(hashKeyGetter); Method rangeKeyGetter = reflector.getPrimaryRangeKeyGetter(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. * * @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 * @deprecated in favor of {@link AttributeTransformer} */ @Deprecated protected Map<String, AttributeValue> untransformAttributes(String hashKey, String rangeKey, Map<String, AttributeValue> attributeValues) { return attributeValues; } private Map<String, AttributeValue> transformAttributes(final AttributeTransformer.Parameters parameters) { if (transformer != null) { return transformer.transform(parameters); } return transformAttributes(parameters.getModelClass(), parameters.getAttributeValues()); } /** * By default, just calls {@link #transformAttributes(String, String, Map)}. * * @param clazz * @param attributeValues * @return the decrypted attribute values * @deprecated in favor of {@link AttributeTransformer} */ @Deprecated protected Map<String, AttributeValue> transformAttributes(Class<?> clazz, Map<String, AttributeValue> attributeValues) { Method hashKeyGetter = reflector.getPrimaryHashKeyGetter(clazz); String hashKeyName = reflector.getAttributeName(hashKeyGetter); Method rangeKeyGetter = reflector.getPrimaryRangeKeyGetter(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 * @deprecated in favor of {@link AttributeTransformer} */ @Deprecated protected Map<String, AttributeValue> transformAttributes(String hashKey, String rangeKey, Map<String, AttributeValue> attributeValues) { return attributeValues; } private Map<String, AttributeValueUpdate> transformAttributeUpdates(final Class<?> clazz, final Map<String, AttributeValue> keys, final Map<String, AttributeValueUpdate> updateValues, final DynamoDBMapperConfig config) { Map<String, AttributeValue> item = convertToItem(updateValues); HashSet<String> keysAdded = new HashSet<String>(); for (Map.Entry<String, AttributeValue> e : keys.entrySet()) { if (!item.containsKey(e.getKey())) { keysAdded.add(e.getKey()); item.put(e.getKey(), e.getValue()); } } AttributeTransformer.Parameters<?> parameters = toParameters(item, true, clazz, config); String hashKey = parameters.getHashKeyName(); if (!item.containsKey(hashKey)) { item.put(hashKey, keys.get(hashKey)); } item = transformAttributes(parameters); for (Map.Entry<String, AttributeValue> entry : item.entrySet()) { if (keysAdded.contains(entry.getKey())) { // This was added in for context before calling // transformAttributes, but isn't actually being changed. continue; } AttributeValueUpdate update = updateValues.get(entry.getKey()); if (update != null) { update.getValue().withB(entry.getValue().getB()).withBS(entry.getValue().getBS()) .withN(entry.getValue().getN()).withNS(entry.getValue().getNS()) .withS(entry.getValue().getS()).withSS(entry.getValue().getSS()); } else { updateValues.put(entry.getKey(), new AttributeValueUpdate(entry.getValue(), "PUT")); } } return updateValues; } private void pauseExponentially(int retries) { if (retries == 0) { return; } Random random = new Random(); long delay = 0; long scaleFactor = 500 + random.nextInt(100); delay = (long) (Math.pow(2, retries) * scaleFactor); delay = Math.min(delay, MAX_BACKOFF_IN_MILLISECONDS); try { Thread.sleep(delay); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new AmazonClientException(e.getMessage(), e); } } /** * Returns a new map object that merges the two sets of expected value * conditions (user-specified or imposed by the internal implementation of * DynamoDBMapper). Internal assertion on an attribute will be overridden by * any user-specified condition on the same attribute. * <p> * Exception is thrown if the two sets of conditions cannot be combined * together. */ private static Map<String, ExpectedAttributeValue> mergeExpectedAttributeValueConditions( Map<String, ExpectedAttributeValue> internalAssertions, Map<String, ExpectedAttributeValue> userProvidedConditions, String userProvidedConditionOperator) { // If any of the condition map is null, simply return a copy of the other one. if (internalAssertions == null && userProvidedConditions == null) { return null; } else if (internalAssertions == null) { return new HashMap<String, ExpectedAttributeValue>(userProvidedConditions); } else if (userProvidedConditions == null) { return new HashMap<String, ExpectedAttributeValue>(internalAssertions); } // Start from a copy of the internal conditions Map<String, ExpectedAttributeValue> mergedExpectedValues = new HashMap<String, ExpectedAttributeValue>( internalAssertions); // Remove internal conditions that are going to be overlaid by user-provided ones. for (String attrName : userProvidedConditions.keySet()) { mergedExpectedValues.remove(attrName); } // All the generated internal conditions must be joined by AND. // Throw an exception if the user specifies an OR operator, and that the // internal conditions are not totally overlaid by the user-provided // ones. if (ConditionalOperator.OR.toString().equals(userProvidedConditionOperator) && !mergedExpectedValues.isEmpty()) { throw new IllegalArgumentException("Unable to assert the value of the fields " + mergedExpectedValues.keySet() + ", since the expected value conditions cannot be combined " + "with user-specified conditions joined by \"OR\". You can use SaveBehavior.CLOBBER to " + "skip the assertion on these fields."); } mergedExpectedValues.putAll(userProvidedConditions); return mergedExpectedValues; } static <X extends AmazonWebServiceRequest> X applyUserAgent(X request) { request.getRequestClientOptions().appendUserAgent(USER_AGENT); return request; } /** * The return type of batchWrite, batchDelete and batchSave. It contains the information about the unprocessed items * and the exception causing the failure. * */ public static class FailedBatch { private Map<String, java.util.List<WriteRequest>> unprocessedItems; private Exception exception; public void setUnprocessedItems(Map<String, java.util.List<WriteRequest>> unprocessedItems) { this.unprocessedItems = unprocessedItems; } public Map<String, java.util.List<WriteRequest>> getUnprocessedItems() { return unprocessedItems; } public void setException(Exception excetpion) { this.exception = excetpion; } public Exception getException() { return exception; } } /** * Returns the underlying {@link S3ClientCache} for accessing S3. */ public S3ClientCache getS3ClientCache() { return s3cc; } /** * Creates an S3Link with the specified bucket name and key using the * default S3 region. * This method requires the mapper to have been initialized with the * necessary credentials for accessing S3. * * @throws IllegalStateException if the mapper has not been constructed * with the necessary S3 AWS credentials. */ public S3Link createS3Link(String bucketName, String key) { return createS3Link(null, bucketName, key); } /** * Creates an S3Link with the specified region, bucket name and key. * This method requires the mapper to have been initialized with the * necessary credentials for accessing S3. * * @throws IllegalStateException if the mapper has not been constructed * with the necessary S3 AWS credentials. */ public S3Link createS3Link(Region s3region, String bucketName, String key) { if (s3cc == null) { throw new IllegalStateException("Mapper must be constructed with S3 AWS Credentials to create S3Link"); } return new S3Link(s3cc, s3region, bucketName, key); } /** * Parse the given POJO class and return the CreateTableRequest for the * DynamoDB table it represents. Note that the returned request does not * include the required ProvisionedThroughput parameters for the primary * table and the GSIs, and that all secondary indexes are initialized with * the default projection type - KEY_ONLY. */ public CreateTableRequest generateCreateTableRequest(Class<?> clazz) { return schemaParser.parseTablePojoToCreateTableRequest(clazz, config, reflector); } }