org.iternine.jeppetto.dao.dynamodb.DynamoDBQueryModelDAO.java Source code

Java tutorial

Introduction

Here is the source code for org.iternine.jeppetto.dao.dynamodb.DynamoDBQueryModelDAO.java

Source

/*
 * Copyright (c) 2011-2014 Jeppetto and Jonathan Thompson
 *
 * 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://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License 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 org.iternine.jeppetto.dao.dynamodb;

import org.iternine.jeppetto.dao.AccessControlContextProvider;
import org.iternine.jeppetto.dao.Condition;
import org.iternine.jeppetto.dao.ConditionType;
import org.iternine.jeppetto.dao.FailedBatchException;
import org.iternine.jeppetto.dao.JeppettoException;
import org.iternine.jeppetto.dao.NoSuchItemException;
import org.iternine.jeppetto.dao.OptimisticLockException;
import org.iternine.jeppetto.dao.Pair;
import org.iternine.jeppetto.dao.Projection;
import org.iternine.jeppetto.dao.ProjectionType;
import org.iternine.jeppetto.dao.QueryModel;
import org.iternine.jeppetto.dao.QueryModelDAO;
import org.iternine.jeppetto.dao.Sort;
import org.iternine.jeppetto.dao.SortDirection;
import org.iternine.jeppetto.dao.TooManyItemsException;
import org.iternine.jeppetto.dao.UpdateBehaviorDescriptor;
import org.iternine.jeppetto.dao.ResultFromUpdate;
import org.iternine.jeppetto.dao.dynamodb.expression.ConditionExpressionBuilder;
import org.iternine.jeppetto.dao.dynamodb.expression.ProjectionExpressionBuilder;
import org.iternine.jeppetto.dao.dynamodb.expression.UpdateExpressionBuilder;
import org.iternine.jeppetto.dao.dynamodb.iterable.BatchGetIterable;
import org.iternine.jeppetto.dao.dynamodb.iterable.DynamoDBIterable;
import org.iternine.jeppetto.dao.dynamodb.iterable.QueryIterable;
import org.iternine.jeppetto.dao.dynamodb.iterable.ScanIterable;
import org.iternine.jeppetto.dao.id.IdGenerator;
import org.iternine.jeppetto.dao.updateobject.UpdateObject;
import org.iternine.jeppetto.enhance.Enhancer;

import com.amazonaws.AmazonClientException;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.model.AttributeValue;
import com.amazonaws.services.dynamodbv2.model.BatchGetItemRequest;
import com.amazonaws.services.dynamodbv2.model.ConditionalCheckFailedException;
import com.amazonaws.services.dynamodbv2.model.DeleteItemRequest;
import com.amazonaws.services.dynamodbv2.model.GetItemRequest;
import com.amazonaws.services.dynamodbv2.model.GetItemResult;
import com.amazonaws.services.dynamodbv2.model.GlobalSecondaryIndexDescription;
import com.amazonaws.services.dynamodbv2.model.KeySchemaElement;
import com.amazonaws.services.dynamodbv2.model.KeyType;
import com.amazonaws.services.dynamodbv2.model.KeysAndAttributes;
import com.amazonaws.services.dynamodbv2.model.LocalSecondaryIndexDescription;
import com.amazonaws.services.dynamodbv2.model.PutItemRequest;
import com.amazonaws.services.dynamodbv2.model.QueryRequest;
import com.amazonaws.services.dynamodbv2.model.ReturnValue;
import com.amazonaws.services.dynamodbv2.model.ScanRequest;
import com.amazonaws.services.dynamodbv2.model.TableDescription;
import com.amazonaws.services.dynamodbv2.model.UpdateItemRequest;

import com.amazonaws.services.dynamodbv2.model.UpdateItemResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * An implementation of the QueryModelDAO interface that works atop DynamoDB.
 *
 * @param <T> Persistent class
 * @param <ID> ID type of the persistent class.
 */
public class DynamoDBQueryModelDAO<T, ID> implements QueryModelDAO<T, ID> {

    //-------------------------------------------------------------
    // Constants
    //-------------------------------------------------------------

    private static final Logger logger = LoggerFactory.getLogger(DynamoDBQueryModelDAO.class);

    //-------------------------------------------------------------
    // Variables - Private
    //-------------------------------------------------------------

    private final Class<T> entityClass;
    private final AmazonDynamoDB dynamoDB;
    private final String tableName;
    private final IdGenerator<ID> idGenerator;
    private final boolean consistentRead;
    private final String optimisticLockField;
    private final boolean enableScans;

    private final String hashKeyField;
    private final String rangeKeyField;
    private final String projectionExpression;
    private final Map<String, String> projectionExpressionNames;
    private final Map<String, Map<String, IndexData>> indexes;
    private final Map<String, Map<String, IndexData>> baseIndexOnly;
    private final Enhancer<T> persistableEnhancer;
    private final Enhancer<? extends T> updateObjectEnhancer;
    private final String uniqueIdConditionExpression;

    //-------------------------------------------------------------
    // Constructors
    //-------------------------------------------------------------

    protected DynamoDBQueryModelDAO(Class<T> entityClass, Map<String, Object> daoProperties) {
        this(entityClass, daoProperties, null);
    }

    @SuppressWarnings({ "unchecked", "UnusedParameters" })
    protected DynamoDBQueryModelDAO(Class<T> entityClass, Map<String, Object> daoProperties,
            AccessControlContextProvider accessControlContextProvider) {
        this.entityClass = entityClass;
        this.dynamoDB = (AmazonDynamoDB) daoProperties.get("db");
        this.tableName = daoProperties.containsKey("tableName") ? (String) daoProperties.get("tableName")
                : entityClass.getSimpleName();
        this.idGenerator = (IdGenerator<ID>) daoProperties.get("idGenerator");
        this.consistentRead = Boolean.parseBoolean((String) daoProperties.get("consistentRead")); // null okay - defaults to false
        this.optimisticLockField = (String) daoProperties.get("optimisticLockField");
        this.enableScans = Boolean.parseBoolean((String) daoProperties.get("enableScans")); // null okay - defaults to false

        TableDescription tableDescription = dynamoDB.describeTable(tableName).getTable();

        Pair<String, String> primaryKeyAttributeNames = getKeyAttributeNames(tableDescription.getKeySchema());
        this.hashKeyField = primaryKeyAttributeNames.getFirst();
        this.rangeKeyField = primaryKeyAttributeNames.getSecond();

        ProjectionExpressionBuilder projectionExpressionBuilder;
        if (Boolean.parseBoolean((String) daoProperties.get("projectionObject"))) { // null okay - defaults to false
            projectionExpressionBuilder = new ProjectionExpressionBuilder(entityClass, hashKeyField, rangeKeyField,
                    optimisticLockField);
            this.projectionExpression = projectionExpressionBuilder.getExpression();
            this.projectionExpressionNames = projectionExpressionBuilder.getExpressionAttributeNames();
        } else {
            projectionExpressionBuilder = null;
            this.projectionExpression = null;
            this.projectionExpressionNames = Collections.emptyMap();
        }

        List<String> keyFields = rangeKeyField == null ? Collections.singletonList(hashKeyField)
                : Arrays.asList(hashKeyField, rangeKeyField);
        IndexData baseIndexData = new IndexData(null, keyFields, true);
        this.baseIndexOnly = Collections.singletonMap(hashKeyField,
                Collections.singletonMap(rangeKeyField, baseIndexData));
        this.indexes = processIndexes(tableDescription, projectionExpressionBuilder, baseIndexData);
        this.persistableEnhancer = EnhancerHelper.getPersistableEnhancer(entityClass);

        String updateObjectClassName = (String) daoProperties.get("updateObject");
        if (updateObjectClassName == null) {
            this.updateObjectEnhancer = EnhancerHelper.getUpdateObjectEnhancer(entityClass);
        } else {
            try {
                Class updateObjectClass = Class.forName(updateObjectClassName);

                if (!entityClass.isAssignableFrom(updateObjectClass)) {
                    throw new JeppettoException(
                            String.format("Invalid UpdateObject type. %s does not subclass entity type %s",
                                    updateObjectClassName, entityClass.getName()));
                }

                this.updateObjectEnhancer = (Enhancer<? extends T>) EnhancerHelper
                        .getUpdateObjectEnhancer(updateObjectClass);
            } catch (ClassNotFoundException e) {
                throw new JeppettoException(e);
            }
        }

        if (Boolean.parseBoolean((String) daoProperties.get("verifyUniqueIds"))) { // null okay - defaults to false
            ConditionExpressionBuilder conditionExpressionBuilder = new ConditionExpressionBuilder()
                    .with(hashKeyField, new DynamoDBConstraint(DynamoDBOperator.IsNull));

            if (rangeKeyField != null) {
                conditionExpressionBuilder.with(rangeKeyField, new DynamoDBConstraint(DynamoDBOperator.IsNull));
            }

            this.uniqueIdConditionExpression = conditionExpressionBuilder.getExpression(); // No attribute values needed
        } else {
            this.uniqueIdConditionExpression = null;
        }
    }

    //-------------------------------------------------------------
    // Implementation - GenericDAO
    //-------------------------------------------------------------

    @Override
    public T findById(ID id) throws NoSuchItemException, JeppettoException {
        GetItemResult result;

        try {
            GetItemRequest getItemRequest = new GetItemRequest(tableName, getKeyFrom(id), consistentRead);

            getItemRequest.setProjectionExpression(projectionExpression);

            if (!projectionExpressionNames.isEmpty()) {
                getItemRequest.setExpressionAttributeNames(projectionExpressionNames);
            }

            result = dynamoDB.getItem(getItemRequest);
        } catch (AmazonClientException e) {
            throw new JeppettoException(e);
        }

        if (result.getItem() == null) {
            throw new NoSuchItemException(entityClass.getSimpleName(), id.toString());
        }

        T t = ConversionUtil.getObjectFromItem(result.getItem(), entityClass);

        ((DynamoDBPersistable) t).__markPersisted(dynamoDB.toString());

        return t;
    }

    @Override
    public Iterable<T> findByIds(ID... ids) throws JeppettoException {
        Collection<Map<String, AttributeValue>> keys = new ArrayList<Map<String, AttributeValue>>();

        for (ID id : ids) {
            keys.add(getKeyFrom(id));
        }

        KeysAndAttributes keysAndAttributes = new KeysAndAttributes().withKeys(keys);

        keysAndAttributes.setConsistentRead(consistentRead);
        keysAndAttributes.setProjectionExpression(projectionExpression);

        if (!projectionExpressionNames.isEmpty()) {
            keysAndAttributes.setExpressionAttributeNames(projectionExpressionNames);
        }

        BatchGetItemRequest batchGetItemRequest = new BatchGetItemRequest()
                .withRequestItems(Collections.singletonMap(tableName, keysAndAttributes));

        return new BatchGetIterable<T>(dynamoDB, persistableEnhancer, batchGetItemRequest, tableName);
    }

    @Override
    public Iterable<T> findAll() throws JeppettoException {
        return findUsingQueryModel(new QueryModel());
    }

    @Override
    public void save(T entity) throws OptimisticLockException, JeppettoException {
        DynamoDBPersistable dynamoDBPersistable = (DynamoDBPersistable) persistableEnhancer.enhance(entity);

        if (!dynamoDBPersistable.__isPersisted(dynamoDB.toString())) {
            if (optimisticLockField != null) {
                dynamoDBPersistable.__put(optimisticLockField, new AttributeValue().withN("0"));
            }

            saveItem(dynamoDBPersistable);
        } else {
            ConditionExpressionBuilder conditionExpressionBuilder;

            if (optimisticLockField != null) {
                AttributeValue attributeValue = (AttributeValue) dynamoDBPersistable.__get(optimisticLockField);
                int optimisticLockVersion;

                if (attributeValue != null) {
                    optimisticLockVersion = Integer.parseInt(attributeValue.getN());

                    conditionExpressionBuilder = new ConditionExpressionBuilder();

                    conditionExpressionBuilder.with(optimisticLockField,
                            new DynamoDBConstraint(DynamoDBOperator.Equal, optimisticLockVersion));
                } else {
                    optimisticLockVersion = -1;

                    conditionExpressionBuilder = null;
                }

                dynamoDBPersistable.__put(optimisticLockField,
                        new AttributeValue().withN(Integer.toString(optimisticLockVersion + 1)));
            } else {
                conditionExpressionBuilder = null;
            }

            try {
                UpdateExpressionBuilder updateExpressionBuilder = new UpdateExpressionBuilder(dynamoDBPersistable);

                updateItem(getKeyFrom(dynamoDBPersistable), updateExpressionBuilder, conditionExpressionBuilder,
                        ResultFromUpdate.ReturnNone);
            } catch (JeppettoException e) {
                if (optimisticLockField != null && e.getCause() instanceof ConditionalCheckFailedException) {
                    throw new OptimisticLockException(e.getCause());
                } else {
                    throw e;
                }
            }
        }

        dynamoDBPersistable.__markPersisted(dynamoDB.toString());
    }

    @Override
    public void delete(T entity) throws JeppettoException {
        if (entity == null) {
            throw new JeppettoException("entity is null; nothing to delete.");
        }

        deleteItem(getKeyFrom((DynamoDBPersistable) persistableEnhancer.enhance(entity)));
    }

    @Override
    public void deleteById(ID id) throws JeppettoException {
        if (id == null) {
            throw new JeppettoException("id is null; unable to delete entity.");
        }

        deleteItem(getKeyFrom(id));
    }

    @Override
    public void deleteByIds(ID... ids) throws FailedBatchException, JeppettoException {
        List<ID> succeeded = new ArrayList<ID>();
        Map<ID, Exception> failed = new LinkedHashMap<ID, Exception>();

        for (ID id : ids) {
            try {
                deleteItem(getKeyFrom(id));

                succeeded.add(id);
            } catch (Exception e) {
                //noinspection ThrowableResultOfMethodCallIgnored
                failed.put(id, e);
            }
        }

        if (failed.size() > 0) {
            throw new FailedBatchException("Unable to delete all items", succeeded, failed);
        }
    }

    @Override
    public <U extends T> U getUpdateObject() {
        //noinspection unchecked
        return (U) updateObjectEnhancer.newInstance();
    }

    @Override
    public <U extends T> T updateById(U updateObject, ID id) throws JeppettoException {
        return updateItem(getKeyFrom(id), new UpdateExpressionBuilder((UpdateObject) updateObject), null,
                getResultFromUpdate(updateObject));
    }

    @Override
    public <U extends T> Iterable<T> updateByIds(U updateObject, ID... ids)
            throws FailedBatchException, JeppettoException {
        List<?> succeeded;
        Map<ID, Exception> failed = new LinkedHashMap<ID, Exception>();
        ResultFromUpdate resultFromUpdate = getResultFromUpdate(updateObject);

        if (resultFromUpdate == ResultFromUpdate.ReturnNone) {
            succeeded = new ArrayList<ID>();
        } else {
            succeeded = new ArrayList<T>();
        }

        UpdateExpressionBuilder updateExpressionBuilder = new UpdateExpressionBuilder((UpdateObject) updateObject);
        for (ID id : ids) {
            try {
                T t = updateItem(getKeyFrom(id), updateExpressionBuilder, null, resultFromUpdate);

                if (resultFromUpdate == ResultFromUpdate.ReturnNone) {
                    //noinspection unchecked
                    ((List<ID>) succeeded).add(id);
                } else {
                    //noinspection unchecked
                    ((List<T>) succeeded).add(t);
                }
            } catch (Exception e) {
                //noinspection ThrowableResultOfMethodCallIgnored
                failed.put(id, e);
            }
        }

        if (failed.size() > 0) {
            throw new FailedBatchException("Unable to update all items", succeeded, failed);
        }

        //noinspection unchecked
        return resultFromUpdate == ResultFromUpdate.ReturnNone ? null : (Iterable<T>) succeeded;
    }

    @Override
    public void flush() throws JeppettoException {
        // Flush not required.
    }

    //-------------------------------------------------------------
    // Implementation - QueryModelDAO
    //-------------------------------------------------------------

    @Override
    public T findUniqueUsingQueryModel(QueryModel queryModel)
            throws NoSuchItemException, TooManyItemsException, JeppettoException {
        DynamoDBIterable<T> dynamoDBIterable = (DynamoDBIterable<T>) findUsingQueryModel(queryModel);

        dynamoDBIterable.setLimit(1);

        Iterator<T> results = dynamoDBIterable.iterator();

        if (!results.hasNext()) {
            throw new NoSuchItemException();
        }

        T result = results.next();

        if (dynamoDBIterable.hasResultsPastLimit()) {
            throw new TooManyItemsException();
        }

        return result;
    }

    @Override
    public Iterable<T> findUsingQueryModel(QueryModel queryModel) throws JeppettoException {
        ConditionExpressionBuilder conditionExpressionBuilder = new ConditionExpressionBuilder(queryModel, indexes);

        if (conditionExpressionBuilder.hasHashKeyCondition()) {
            return queryItems(queryModel, conditionExpressionBuilder);
        } else if (enableScans) {
            logger.info("Condition does not specify a hash key -- using 'scan' to search.");

            conditionExpressionBuilder.convertRangeKeyConditionToExpression();

            return scanItems(queryModel, conditionExpressionBuilder);
        } else {
            throw new JeppettoException("Find cannot be satisfied without a scan and scans have not been enabled."
                    + "  Configure this DAO with 'enableScans' = true to allow this.");
        }
    }

    @Override
    public Object projectUsingQueryModel(QueryModel queryModel) throws JeppettoException {
        // TODO: handle count case.  not sure about other projections...

        throw new UnsupportedOperationException("Projections on DynamoDB are not currently supported.");
    }

    @Override
    public void deleteUsingQueryModel(QueryModel queryModel) throws JeppettoException {
        Iterable<T> matches = findUsingQueryModel(queryModel);

        // Would ideally catch individual exceptions and throw a FailedBatchException.  Unfortunately, we don't have an easy
        // way to convert the key from a match to an ID object, which is what the exception's Map contains.  Maybe DynamoDB will
        // add support for a query-based delete.
        for (T match : matches) {
            delete(match);
        }
    }

    @Override
    public <U extends T> T updateUniqueUsingQueryModel(U updateObject, QueryModel queryModel)
            throws JeppettoException {
        UpdateExpressionBuilder updateExpressionBuilder = new UpdateExpressionBuilder((UpdateObject) updateObject);
        // For referencing an object, we can only identify an item by its actual range key, not one of the index fields.
        ConditionExpressionBuilder conditionExpressionBuilder = new ConditionExpressionBuilder(queryModel,
                baseIndexOnly);
        ResultFromUpdate resultFromUpdate = getResultFromUpdate(updateObject);
        Map<String, AttributeValue> key;

        try {
            key = conditionExpressionBuilder.getKey();
        } catch (NullPointerException e) {
            throw new JeppettoException(
                    "DynamoDB only supports updates where the condition uniquely identifies the item by its key.",
                    e);
        }

        return updateItem(key, updateExpressionBuilder, conditionExpressionBuilder, resultFromUpdate);
    }

    @Override
    public <U extends T> Iterable<T> updateUsingQueryModel(U updateObject, QueryModel queryModel)
            throws JeppettoException {
        // DynamoDB only supports updating a single item at a time.
        T t = updateUniqueUsingQueryModel(updateObject, queryModel);

        if (t == null) {
            return null;
        }

        return Collections.singletonList(t);
    }

    @Override
    public Condition buildCondition(String conditionField, ConditionType conditionType, Iterator argsIterator) {
        return new Condition(conditionField,
                DynamoDBOperator.valueOf(conditionType.name()).buildConstraint(argsIterator));
    }

    @Override
    public Projection buildProjection(String projectionField, ProjectionType projectionType,
            Iterator argsIterator) {
        throw new UnsupportedOperationException("Projections on DynamoDB are not currently supported.");
    }

    //-------------------------------------------------------------
    // Methods - Private
    //-------------------------------------------------------------

    private Map<String, Map<String, IndexData>> processIndexes(TableDescription tableDescription,
            ProjectionExpressionBuilder projectionExpressionBuilder, IndexData baseIndexData) {
        // Collect information about the local secondary indexes.  These will be included with the global indexes below.
        Map<String, IndexData> localIndexes;
        List<LocalSecondaryIndexDescription> localSecondaryIndexes = tableDescription.getLocalSecondaryIndexes();
        if (localSecondaryIndexes != null) {
            localIndexes = new HashMap<String, IndexData>(localSecondaryIndexes.size() + 2);

            // We include these as local indexes to make findUsingQueryModel() code below simpler
            localIndexes.put(rangeKeyField, baseIndexData);
            localIndexes.put(null, baseIndexData);

            for (LocalSecondaryIndexDescription description : localSecondaryIndexes) {
                String indexField = getKeyAttributeNames(description.getKeySchema()).getSecond();
                boolean projectsOverEntity = description.getProjection().getProjectionType().equals("ALL")
                        || projectionExpressionBuilder != null
                                && projectionExpressionBuilder.isCoveredBy(description.getProjection());

                List<String> keyFields = new ArrayList<String>(baseIndexData.keyFields);
                keyFields.add(indexField);

                localIndexes.put(indexField,
                        new IndexData(description.getIndexName(), keyFields, projectsOverEntity));
            }
        } else if (rangeKeyField != null) {
            localIndexes = new HashMap<String, IndexData>(2);

            localIndexes.put(rangeKeyField, baseIndexData);
            localIndexes.put(null, baseIndexData);
        } else {
            localIndexes = Collections.singletonMap(null, baseIndexData);
        }

        // Process the global secondary indexes.  When done, add the local index information.
        List<GlobalSecondaryIndexDescription> globalSecondaryIndexes = tableDescription.getGlobalSecondaryIndexes();
        if (globalSecondaryIndexes != null) {
            Map<String, Map<String, IndexData>> indexes = new HashMap<String, Map<String, IndexData>>(
                    globalSecondaryIndexes.size() + 1);

            for (GlobalSecondaryIndexDescription description : globalSecondaryIndexes) {
                Pair<String, String> indexFields = getKeyAttributeNames(description.getKeySchema());
                boolean projectsOverEntity = description.getProjection().getProjectionType().equals("ALL")
                        || projectionExpressionBuilder != null
                                && projectionExpressionBuilder.isCoveredBy(description.getProjection());

                List<String> keyFields = new ArrayList<String>();
                keyFields.add(indexFields.getFirst());
                if (indexFields.getSecond() != null) {
                    keyFields.add(indexFields.getSecond());
                }
                keyFields.add(hashKeyField);
                if (rangeKeyField != null) {
                    keyFields.add(rangeKeyField);
                }

                IndexData indexData = new IndexData(description.getIndexName(), keyFields, projectsOverEntity);

                if (!indexes.containsKey(indexFields.getFirst())) {
                    indexes.put(indexFields.getFirst(), new HashMap<String, IndexData>());
                }

                indexes.get(indexFields.getFirst()).put(indexFields.getSecond(), indexData);

                // In case a query doesn't specify a range key, we still want to select an index for this hash key.
                // If one has already been selected, pick one that projects over this entity to avoid extra DB reads.
                IndexData noRangeKeyIndexData = indexes.get(indexFields.getFirst()).get(null);
                if (noRangeKeyIndexData == null || !noRangeKeyIndexData.projectsOverEntity) {
                    indexes.get(indexFields.getFirst()).put(null, indexData);
                }
            }

            indexes.put(hashKeyField, localIndexes);

            return indexes;
        } else {
            return Collections.singletonMap(hashKeyField, localIndexes);
        }
    }

    private void saveItem(DynamoDBPersistable dynamoDBPersistable) {
        generateIdIfNeeded(dynamoDBPersistable);

        try {
            PutItemRequest putItemRequest = new PutItemRequest().withTableName(tableName)
                    .withItem(ConversionUtil.getItemFromObject(dynamoDBPersistable))
                    .withConditionExpression(uniqueIdConditionExpression);

            dynamoDB.putItem(putItemRequest);
        } catch (Exception e) {
            throw new JeppettoException(e);
        }
    }

    private T updateItem(Map<String, AttributeValue> key, UpdateExpressionBuilder updateExpressionBuilder,
            ConditionExpressionBuilder conditionExpressionBuilder, ResultFromUpdate resultFromUpdate) {
        try {
            UpdateItemRequest updateItemRequest = new UpdateItemRequest().withTableName(tableName).withKey(key)
                    .withUpdateExpression(updateExpressionBuilder.getExpression());

            Map<String, AttributeValue> expressionAttributeValues;
            Map<String, String> expressionAttributeNames;

            if (conditionExpressionBuilder == null) {
                expressionAttributeValues = updateExpressionBuilder.getExpressionAttributeValues();
                expressionAttributeNames = updateExpressionBuilder.getExpressionAttributeNames();
            } else {
                expressionAttributeValues = new LinkedHashMap<String, AttributeValue>();
                expressionAttributeNames = new LinkedHashMap<String, String>();

                expressionAttributeValues.putAll(updateExpressionBuilder.getExpressionAttributeValues());
                expressionAttributeNames.putAll(updateExpressionBuilder.getExpressionAttributeNames());

                expressionAttributeValues.putAll(conditionExpressionBuilder.getExpressionAttributeValues());
                expressionAttributeNames.putAll(conditionExpressionBuilder.getExpressionAttributeNames());

                updateItemRequest.setConditionExpression(conditionExpressionBuilder.getExpression());
            }

            if (!expressionAttributeValues.isEmpty()) {
                updateItemRequest.setExpressionAttributeValues(expressionAttributeValues);
            }

            if (!expressionAttributeNames.isEmpty()) {
                updateItemRequest.setExpressionAttributeNames(expressionAttributeNames);
            }

            if (resultFromUpdate != ResultFromUpdate.ReturnNone) {
                updateItemRequest
                        .setReturnValues(resultFromUpdate == ResultFromUpdate.ReturnPreUpdate ? ReturnValue.ALL_OLD
                                : ReturnValue.ALL_NEW);

                UpdateItemResult result = dynamoDB.updateItem(updateItemRequest);

                T t = ConversionUtil.getObjectFromItem(result.getAttributes(), entityClass);

                ((DynamoDBPersistable) t).__markPersisted(dynamoDB.toString());

                return t;
            } else {
                dynamoDB.updateItem(updateItemRequest);

                return null;
            }
        } catch (Exception e) {
            throw new JeppettoException(e);
        }
    }

    private void deleteItem(Map<String, AttributeValue> key) {
        try {
            dynamoDB.deleteItem(new DeleteItemRequest(tableName, key));
        } catch (Exception e) {
            throw new JeppettoException(e);
        }
    }

    private Iterable<T> queryItems(QueryModel queryModel, ConditionExpressionBuilder conditionExpressionBuilder) {
        QueryRequest queryRequest = new QueryRequest(tableName);

        queryRequest.setKeyConditions(conditionExpressionBuilder.getKeyConditions());
        queryRequest.setConsistentRead(consistentRead);

        if (queryModel.getFirstResult() > 0) {
            logger.warn(
                    "DynamoDB does not support skipping results.  Call setPosition() on DynamoDBIterable instead.");
        }

        if (queryModel.getMaxResults() > 0) {
            queryRequest.setLimit(queryModel.getMaxResults());
        }

        List<String> keyFields = applyIndexAndGetKeyFields(conditionExpressionBuilder, queryRequest,
                queryModel.getSorts());
        applyExpressions(conditionExpressionBuilder, queryRequest);

        return new QueryIterable<T>(dynamoDB, persistableEnhancer, queryRequest, keyFields.get(0), keyFields);
    }

    private List<String> applyIndexAndGetKeyFields(ConditionExpressionBuilder conditionExpressionBuilder,
            QueryRequest queryRequest, List<Sort> sorts) {
        String hashKey = conditionExpressionBuilder.getHashKey();
        String rangeKey = conditionExpressionBuilder.getRangeKey();
        IndexData indexData;

        if (sorts == null || sorts.isEmpty()) {
            indexData = indexes.get(hashKey).get(rangeKey);
        } else if (sorts.size() == 1) {
            Sort sort = sorts.get(0);
            String sortKey = sort.getField();

            // DynamoDB can only sort on the effective range key.  If a range key is specified, ensure the range key and
            // sort key are the same.
            if (rangeKey != null && !rangeKey.equals(sortKey)) {
                throw new JeppettoException(
                        "DynamoDB can only sort on the effective range key. Unable to sort on: " + sortKey);
            }

            queryRequest.setScanIndexForward(sort.getSortDirection() == SortDirection.Ascending);

            // Index is based off the sort key
            indexData = indexes.get(hashKey).get(sortKey);
        } else {
            throw new JeppettoException("DynamoDB only supports one sort value.");
        }

        if (indexData.indexName != null && !indexData.projectsOverEntity) {
            logger.warn(
                    "Query using index {} incurs additional costs to fully fetch a {} type. Use a projected object"
                            + " DAO to avoid this overhead.",
                    indexData.indexName, entityClass.getSimpleName());
        }

        queryRequest.setIndexName(indexData.indexName);

        return indexData.keyFields;
    }

    private void applyExpressions(ConditionExpressionBuilder conditionExpressionBuilder,
            QueryRequest queryRequest) {
        Map<String, String> expressionAttributeNames;

        queryRequest.setProjectionExpression(projectionExpression);

        if (conditionExpressionBuilder.hasExpression()) {
            queryRequest.setFilterExpression(conditionExpressionBuilder.getExpression());

            if (!conditionExpressionBuilder.getExpressionAttributeValues().isEmpty()) {
                queryRequest
                        .setExpressionAttributeValues(conditionExpressionBuilder.getExpressionAttributeValues());
            }

            if (projectionExpressionNames.isEmpty()) {
                expressionAttributeNames = conditionExpressionBuilder.getExpressionAttributeNames();
            } else if (conditionExpressionBuilder.getExpressionAttributeNames().isEmpty()) {
                expressionAttributeNames = projectionExpressionNames;
            } else {
                expressionAttributeNames = new LinkedHashMap<String, String>();
                expressionAttributeNames.putAll(conditionExpressionBuilder.getExpressionAttributeNames());
                expressionAttributeNames.putAll(projectionExpressionNames);
            }
        } else {
            expressionAttributeNames = projectionExpressionNames;
        }

        if (!expressionAttributeNames.isEmpty()) {
            queryRequest.setExpressionAttributeNames(expressionAttributeNames);
        }
    }

    private Iterable<T> scanItems(QueryModel queryModel, ConditionExpressionBuilder conditionExpressionBuilder) {
        ScanRequest scanRequest = new ScanRequest(tableName);

        if (queryModel.getFirstResult() > 0) {
            logger.warn(
                    "DynamoDB does not support skipping results.  Call setPosition() on DynamoDBIterable instead.");
        }

        if (queryModel.getMaxResults() > 0) {
            scanRequest.setLimit(queryModel.getMaxResults());
        }

        if (queryModel.getSorts() != null) {
            logger.warn("Not able to sort when performing a 'scan' operation.  Ignoring... ");
        }

        Map<String, String> expressionAttributeNames;

        scanRequest.setProjectionExpression(projectionExpression);

        if (conditionExpressionBuilder.hasExpression()) {
            scanRequest.setFilterExpression(conditionExpressionBuilder.getExpression());

            if (!conditionExpressionBuilder.getExpressionAttributeValues().isEmpty()) {
                scanRequest.setExpressionAttributeValues(conditionExpressionBuilder.getExpressionAttributeValues());
            }

            if (projectionExpressionNames.isEmpty()) {
                expressionAttributeNames = conditionExpressionBuilder.getExpressionAttributeNames();
            } else if (conditionExpressionBuilder.getExpressionAttributeNames().isEmpty()) {
                expressionAttributeNames = projectionExpressionNames;
            } else {
                expressionAttributeNames = new LinkedHashMap<String, String>();
                expressionAttributeNames.putAll(conditionExpressionBuilder.getExpressionAttributeNames());
                expressionAttributeNames.putAll(projectionExpressionNames);
            }
        } else {
            expressionAttributeNames = projectionExpressionNames;
        }

        if (!expressionAttributeNames.isEmpty()) {
            scanRequest.setExpressionAttributeNames(expressionAttributeNames);
        }

        return new ScanIterable<T>(dynamoDB, persistableEnhancer, scanRequest,
                rangeKeyField == null ? Collections.singleton(hashKeyField)
                        : Arrays.asList(hashKeyField, rangeKeyField));
    }

    private <U extends T> ResultFromUpdate getResultFromUpdate(U updateObject) {
        if (UpdateBehaviorDescriptor.class.isAssignableFrom(updateObject.getClass())) {
            ResultFromUpdate resultFromUpdate = ((UpdateBehaviorDescriptor) updateObject).getResultFromUpdate();

            return resultFromUpdate != null ? resultFromUpdate : ResultFromUpdate.ReturnNone;
        } else {
            return ResultFromUpdate.ReturnNone;
        }
    }

    private void generateIdIfNeeded(DynamoDBPersistable dynamoDBPersistable) {
        if (dynamoDBPersistable.__get(hashKeyField) != null
        /* && rangeKeyField != null && dynamoDBPersistable.__get(rangeKeyField) != null */) {
            return;
        }

        if (idGenerator == null) {
            throw new JeppettoException("No id provided, and no id generator available.");
        }

        // TODO: handle case when part of the key is there (e.g. code generates range key, but wants to generate hash key)
        // Can't blindly use getKeyFrom since a single generated value may be for the range key...
        dynamoDBPersistable.__putAll(getKeyFrom(idGenerator.generateId()));
    }

    private AttributeValue getAttributeValue(Object value) {
        if (Number.class.isAssignableFrom(value.getClass())) {
            return new AttributeValue().withN(value.toString());
        } else {
            return new AttributeValue(value.toString());
        }
    }

    private Pair<String, String> getKeyAttributeNames(List<KeySchemaElement> keySchema) {
        Pair<String, String> keyAttributes = new Pair<String, String>();

        for (KeySchemaElement keySchemaElement : keySchema) {
            if (keySchemaElement.getKeyType().equals(KeyType.HASH.name())) {
                keyAttributes.setFirst(keySchemaElement.getAttributeName());
            } else {
                keyAttributes.setSecond(keySchemaElement.getAttributeName());
            }
        }

        return keyAttributes;
    }

    private Map<String, AttributeValue> getKeyFrom(ID id) {
        Map<String, AttributeValue> key;

        if (Pair.class.isAssignableFrom(id.getClass())) {
            key = new HashMap<String, AttributeValue>(2);

            key.put(hashKeyField, getAttributeValue(((Pair) id).getFirst()));
            key.put(rangeKeyField, getAttributeValue(((Pair) id).getSecond()));
        } else {
            key = Collections.singletonMap(hashKeyField, getAttributeValue(id));
        }

        return key;
    }

    private Map<String, AttributeValue> getKeyFrom(DynamoDBPersistable dynamoDBPersistable) {
        Map<String, AttributeValue> key;

        if (rangeKeyField != null) {
            key = new HashMap<String, AttributeValue>(2);

            key.put(hashKeyField, ConversionUtil.toAttributeValue(dynamoDBPersistable.__get(hashKeyField)));
            key.put(rangeKeyField, ConversionUtil.toAttributeValue(dynamoDBPersistable.__get(rangeKeyField)));
        } else {
            key = Collections.singletonMap(hashKeyField,
                    ConversionUtil.toAttributeValue(dynamoDBPersistable.__get(hashKeyField)));
        }

        return key;
    }

    //-------------------------------------------------------------
    // Inner Classes
    //-------------------------------------------------------------

    public static class IndexData {
        public String indexName;
        public List<String> keyFields;
        public boolean projectsOverEntity;

        private IndexData(String indexName, List<String> keyFields, boolean projectsOverEntity) {
            this.indexName = indexName;
            this.keyFields = keyFields;
            this.projectsOverEntity = projectsOverEntity;
        }
    }
}