Aws-sdk-android: Search class seems weird in DynamoDB (aws-android-sdk-ddb-document)

Created on 3 Dec 2018  路  6Comments  路  Source: aws-amplify/aws-sdk-android

The class Search, in package com.amazonaws.mobileconnectors.dynamodbv2.document has some issues in usage:

1- It seams that it doesn't setting the ScanIndexForward appropriately with withIsBackwardSearch. So it never returns sorted descending, only ascending.
2- The getCount always returns -1
3- There isn't any way to know that the getNextQueryResultSet reached the end
4- The content size returned in getNextQueryResultSet oscillates and never returns exactly the limit defined in withLimit

There's some way to solve theese problems?

Version in gradle:
implementation 'com.amazonaws:aws-android-sdk-ddb-document:2.8.4'

DynamoDB Needs Info from Requester Usage Question closing-soon-if-no-response

All 6 comments

Hello @rafagan

Thanks for the feedback. aws-android-sdk-ddb-document is not actively maintained and I recommend you use AWS AppSync for these use-cases. I will take these issues to our product team for discussion.

Thanks,
Rohan

Sorry, but I dont know what is AWS AppSync. Where is this lib?

I already solved 1 of the problems in the class. If you wish I can send here or as a pool request.

I did some corrections, enabling to stop the search loop in getNextQueryResultSet and working with ScanIndexForward appropriately.

The problems with getCount and weird list sizes persists.

package com.amazonaws.mobileconnectors.dynamodbv2.document;

import com.amazonaws.mobileconnectors.dynamodbv2.document.datatype.Document;
import com.amazonaws.services.dynamodbv2.model.AttributeValue;
import com.amazonaws.services.dynamodbv2.model.Condition;
import com.amazonaws.services.dynamodbv2.model.ConditionalOperator;
import com.amazonaws.services.dynamodbv2.model.GlobalSecondaryIndexDescription;
import com.amazonaws.services.dynamodbv2.model.KeySchemaElement;
import com.amazonaws.services.dynamodbv2.model.LocalSecondaryIndexDescription;
import com.amazonaws.services.dynamodbv2.model.QueryRequest;
import com.amazonaws.services.dynamodbv2.model.QueryResult;
import com.amazonaws.services.dynamodbv2.model.ScanRequest;
import com.amazonaws.services.dynamodbv2.model.ScanResult;
import com.amazonaws.services.dynamodbv2.model.Select;
import com.amazonaws.util.StringUtils;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import android.util.Log;

/**
 * Search response object.
 */
@SuppressWarnings("checkstyle:hiddenfield")
public class Search {

    /**
     * The type of search operation.
     */
    public enum SearchType {

        /**
         * Query Operation.
         */
        QUERY("QUERY"),
        /**
         * Scan Operation.
         */
        SCAN("SCAN");

        private final String type;

        private SearchType(String type) {
            this.type = type;
        }

        @Override
        public String toString() {
            return this.type;
        }
    }

    private Table table;

    /**
     * Name of the table being searched
     */
    private String tableName;

    /**
     * Whether to collect GetNextSet and GetRemaining results in Matches
     * property. Default is true. If set to false, Matches will always be empty.
     */
    private boolean collectResults = true;

    /**
     * Upper limit on the number of items returned. For Queries: upper limit on
     * the number of items returned. For Scan: limit on the number of items
     * scanned. Maps directly to Limit property on Query and Scan operations.
     */
    private int limit = 1;

    /**
     * <p>
     * The key expression that is evaluated for each item of a query. This
     * applies only to Query operations.
     * </p>
     */
    private Expression keyExpression;

    /**
     * <p>
     * The filter expression that is evaluated for each item. This applies to
     * Query and Scan operations.
     * </p>
     */
    private Expression filterExpression;

    /**
     * Filter for the search operation This applies to Query and Scan
     * operations.
     */
    private Filter filter;

    /**
     * Conditional operator for the search operation
     */
    private ConditionalOperator conditionalOperator;

    /**
     * List of attribute names to retrieve
     */
    private List<String> attributesToGet;

    /**
     * Flag that, if true, indicates that the search is operating in
     * consistent-read mode
     */
    private boolean isConsistentRead;

    /**
     * Flag that, if true, indicates that the search is traversing backwards
     */
    private boolean isBackwardSearch;

    /**
     * Flag that, if true, indicates that the search is done
     */
    public boolean isDone = false;

    /**
     * Key denoting the starting point of the next set of results
     */
    private Map<String, AttributeValue> nextKey;

    /**
     * List of currently found items
     */
    private final List<Document> matches = new ArrayList<Document>();

    /**
     * <p>
     * For parallel <i>Scan</i> requests, <i>TotalSegments</i>represents the
     * total number of segments for a table that is being scanned. Segments are
     * a way to logically divide a table into equally sized portions, for the
     * duration of the <i>Scan</i> request. The value of <i>TotalSegments</i>
     * corresponds to the number of application "workers" (such as threads or
     * processes) that will perform the parallel <i>Scan</i>. For example, if
     * you want to scan a table using four application threads, you would
     * specify a <i>TotalSegments</i> value of 4. The value for
     * <i>TotalSegments</i> must be greater than or equal to 1, and less than or
     * equal to 4096. If you specify a <i>TotalSegments</i> value of 1, the
     * <i>Scan</i> will be sequential rather than parallel. If you specify
     * <i>TotalSegments</i>, you must also specify <i>Segment</i>.
     * <p>
     * <b>Constraints:</b>
     * <ul>
     * <li><term>Range</li>
     * <li>1 - 4096</li>
     * </ul>
     * </p>
     * </p>
     */
    private int totalSegments;

    /**
     * <p>
     * For parallel <i>Scan</i> requests, <i>Segment</i> identifies an
     * individual segment to be scanned by an application "worker" (such as a
     * thread or a process). Each worker issues a <i>Scan</i> request with a
     * distinct value for the segment it will scan. Segment IDs are zero-based,
     * so the first segment is always 0. For example, if you want to scan a
     * table using four application threads, the first thread would specify a
     * <i>Segment</i> value of 0, the second thread would specify 1, and so on.
     * LastEvaluatedKey returned from a parallel scan request must be used with
     * same Segment id in a subsequent operation. The value for <i>Segment</i>
     * must be less than or equal to 0, and less than the value provided for
     * <i>TotalSegments</i>.
     * </p>
     */
    private int segment;

    /**
     * <p>
     * Gets the total number of items that match the search parameters. If
     * IsDone is true and CollectResults is true, returns Matches.Count.
     * Otherwise, makes a call to DynamoDB to find out the number of matching
     * items, without retrieving the items. Count is then cached.
     * </p>
     */
    private int count = -1;

    /**
     * Name of the index to query or scan against.
     */
    private String indexName;

    /**
     * Enum specifying what data to return from query.
     */
    private Select select;

    private final SearchType searchType;

    protected Search(SearchType searchType) {
        this.searchType = searchType;
    }

    protected Search() {
        this.searchType = SearchType.QUERY;
    }

    protected Search withTableName(String tableName) {
        this.tableName = tableName;
        return this;
    }

    protected Search withCollectResults(boolean collectResults) {
        this.collectResults = collectResults;
        return this;
    }

    protected Search withLimit(int limit) {
        this.limit = limit;
        return this;
    }

    protected Search withKeyExpression(Expression keyExpression) {
        this.keyExpression = keyExpression;
        return this;
    }

    protected Search withFilterExpression(Expression filterExpression) {
        this.filterExpression = filterExpression;
        return this;
    }

    protected Search withFilter(Filter filter) {
        this.filter = filter;
        return this;
    }

    protected Search withConditionalOperator(ConditionalOperator conditionalOperator) {
        this.conditionalOperator = conditionalOperator;
        return this;
    }

    protected Search withAttributesToGet(List<String> attributesToGet) {
        this.attributesToGet = attributesToGet;
        return this;
    }

    protected Search withIsConsistentRead(boolean isConsistentRead) {
        this.isConsistentRead = isConsistentRead;
        return this;
    }

    protected Search withIsBackwardSearch(boolean isBackwardSearch) {
        this.isBackwardSearch = isBackwardSearch;
        return this;
    }

    protected Search withNextKey(Map<String, AttributeValue> nextKey) {
        this.nextKey = nextKey;
        return this;
    }

    protected Search withTotalSegments(int totalSegments) {
        this.totalSegments = totalSegments;
        return this;
    }

    protected Search withSegment(int segment) {
        this.segment = segment;
        return this;
    }

    protected Search withIndexName(String indexName) {
        this.indexName = indexName;
        return this;
    }

    protected Search withSelect(Select select) {
        this.select = select;
        return this;
    }

    protected Search withTable(Table table) {
        this.table = table;
        return this;
    }

    /**
     * @return paginated list of {@link Document}
     */
    public List<Document> getNextResultSet() {
        if(isDone) return new ArrayList<Document>();

        switch (this.searchType) {
            case QUERY:
                return getNextQueryResultSet();
            case SCAN:
                return getNextScanResultSet();
            default:
                throw new IllegalStateException("search type other than scan or query");
        }
    }

    private List<Document> getNextScanResultSet() {
        final List<Document> returnValue = new ArrayList<Document>();
        final ScanRequest request = new ScanRequest();
        request.withExclusiveStartKey(nextKey)
                .withAttributesToGet(attributesToGet)
                .withLimit(limit)
                .withTableName(tableName)
                .withConsistentRead(isConsistentRead);

        if (select != null) {
            request.withSelect(select);
        }

        if (this.filter != null) {
            request.withScanFilter(this.filter.toConditions());
        }

        if (!StringUtils.isBlank(this.indexName)) {
            request.withIndexName(indexName);
        }

        if (this.filterExpression != null && this.filterExpression.isSet()) {
            this.filterExpression.applyExpression(request, table);
        }

        if (request.getScanFilter() != null && request.getScanFilter().size() > 1) {
            request.setConditionalOperator(this.conditionalOperator);
        }

        if (this.totalSegments != 0) {
            request.withTotalSegments(totalSegments)
                    .withSegment(segment);
        }

        Table.appendDynamoDBDocumentUserAgentString(request);

        final ScanResult result = table.getClient().scan(request);
        for (final Map<String, AttributeValue> item : result.getItems()) {
            final Document doc = Document.fromAttributeMap(item);
            returnValue.add(doc);
            if (this.collectResults) {
                this.matches.add(doc);
            }
        }
        nextKey = result.getLastEvaluatedKey();
        if (nextKey == null || nextKey.size() == 0) {
            isDone = true;
        }

        return returnValue;
    }

    private List<Document> getNextQueryResultSet() {
        final List<Document> returnValue = new ArrayList<Document>();
        final QueryRequest request = new QueryRequest();

        request.withExclusiveStartKey(nextKey)
                .withAttributesToGet(attributesToGet)
                .withLimit(limit)
                .withTableName(tableName)
                .withConsistentRead(isConsistentRead)
                .withIndexName(this.indexName)
                .withScanIndexForward(this.isBackwardSearch);

        if (select != null) {
            request.withSelect(select);
        }

        Expression.applyExpression(request, table, keyExpression, filterExpression);

        if (this.filter != null) {
            final Map<String, Condition> keyConditions = getKeyConditions((QueryFilter) this.filter,
                    request.getIndexName());
            final Map<String, Condition> filterConditions = getFilterConditions(
                    (QueryFilter) this.filter,
                    request.getIndexName());
            if (!keyConditions.isEmpty()) {
                request.withKeyConditions(keyConditions);
            }
            if (!filterConditions.isEmpty()) {
                request.withQueryFilter(filterConditions);
            }
        } else {
            request.withKeyConditions(null).withQueryFilter(null);
        }

        if (request.getQueryFilter() != null && request.getQueryFilter().size() > 1) {
            request.withConditionalOperator(this.conditionalOperator);
        } else {
            request.withConditionalOperator((String) null);
        }

        Table.appendDynamoDBDocumentUserAgentString(request);

        final QueryResult result = table.getClient().query(request);
        for (final Map<String, AttributeValue> item : result.getItems()) {
            final Document doc = Document.fromAttributeMap(item);
            returnValue.add(doc);
            if (this.collectResults) {
                this.matches.add(doc);
            }
        }
        nextKey = result.getLastEvaluatedKey();
        if (nextKey == null || nextKey.size() == 0) {
            isDone = true;
        }

        return returnValue;
    }

    /**
     * @return complete list of {@link Document}
     */
    public List<Document> getAllResults() {
        final List<Document> returnValue = new ArrayList<Document>();

        while (!this.isDone) {
            returnValue.addAll(getNextResultSet());
        }

        return returnValue;
    }

    private Map<String, Condition> getKeyConditions(QueryFilter filter, String indexName) {
        final Map<String, Condition> keyConditions = new HashMap<String, Condition>();
        final Map<String, Condition> conditions = filter.toConditions();
        for (final Entry<String, Condition> entry : conditions.entrySet()) {
            final String attributeName = entry.getKey();
            final Condition condition = entry.getValue();
            if (isKeyAttribute(table, indexName, attributeName)) {
                keyConditions.put(attributeName, condition);
            }
        }

        return keyConditions;
    }

    private Map<String, Condition> getFilterConditions(QueryFilter filter, String indexName) {
        final Map<String, Condition> filterConditions = new HashMap<String, Condition>();
        final Map<String, Condition> conditions = filter.toConditions();
        for (final Entry<String, Condition> entry : conditions.entrySet()) {
            final String attributeName = entry.getKey();
            final Condition condition = entry.getValue();
            if (!isKeyAttribute(table, indexName, attributeName)) {
                filterConditions.put(attributeName, condition);
            }
        }

        return filterConditions;
    }

    private static boolean isKeyAttribute(Table table, String indexName, String attributeName) {
        GlobalSecondaryIndexDescription gsi = null;
        final LocalSecondaryIndexDescription lsi;

        if (StringUtils.isBlank(indexName)) {
            return table.getKeys().containsKey(attributeName);
        } else if (table.getGlobalSecondaryIndexes().get(indexName) != null) {
            gsi = table.getGlobalSecondaryIndexes().get(indexName);
            for (final KeySchemaElement element : gsi.getKeySchema()) {
                return isKeyAttribute(element, attributeName);
            }
        } else if (table.getLocalSecondaryIndexes().get(indexName) != null) {
            lsi = table.getLocalSecondaryIndexes().get(indexName);
            for (final KeySchemaElement element : lsi.getKeySchema()) {
                return isKeyAttribute(element, attributeName);
            }
        } else {
            throw new IllegalStateException(String.format(
                    "Unable to locate index %s on table %s", indexName, table.getTableName()));
        }

        return false;
    }

    private static boolean isKeyAttribute(KeySchemaElement element, String attributeName) {
        return element.getAttributeName().equals(attributeName);
    }

    /**
     * Gets the count of the search result.
     *
     * @return count.
     */
    public int getCount() {
        if (isDone && this.collectResults) {
            return this.matches.size();
        } else {
            if (count != -1) {
                return count;
            } else {
                switch (searchType) {
                    case SCAN: {
                        final ScanRequest request = new ScanRequest();
                        request.withExclusiveStartKey(nextKey)
                                .withTableName(tableName)
                                .withScanFilter(this.filter.toConditions())
                                .withSelect(Select.COUNT)
                                .withConsistentRead(isConsistentRead);

                        if (!StringUtils.isBlank(this.indexName)) {
                            request.withIndexName(indexName);
                        }

                        if (this.filterExpression != null && this.filterExpression.isSet()) {
                            this.filterExpression.applyExpression(request, table);
                        }

                        if (request.getScanFilter() != null && request.getScanFilter().size() > 1) {
                            request.setConditionalOperator(this.conditionalOperator);
                        }

                        if (this.totalSegments != 0) {
                            request.withTotalSegments(totalSegments)
                                    .withSegment(segment);
                        }

                        Table.appendDynamoDBDocumentUserAgentString(request);
                        final ScanResult result = table.getClient().scan(request);
                        count = this.matches.size() + result.getCount();
                        return count;
                    }
                    case QUERY: {
                        final QueryRequest request = new QueryRequest();
                        request.withExclusiveStartKey(nextKey)
                                .withTableName(tableName)
                                .withSelect(Select.COUNT)
                                .withConsistentRead(isConsistentRead)
                                .withIndexName(this.indexName)
                                .withScanIndexForward(this.isBackwardSearch);

                        Expression.applyExpression(request, table, keyExpression, filterExpression);

                        final Map<String, Condition> keyConditions = getKeyConditions(
                                (QueryFilter) this.filter,
                                request.getIndexName());
                        final Map<String, Condition> filterConditions = getFilterConditions(
                                (QueryFilter) this.filter,
                                request.getIndexName());
                        request.withKeyConditions(keyConditions).withQueryFilter(filterConditions);

                        if (request.getQueryFilter() != null
                                && request.getQueryFilter().size() > 1) {
                            request.withConditionalOperator(this.conditionalOperator);
                        }

                        Table.appendDynamoDBDocumentUserAgentString(request);

                        final QueryResult result = table.getClient().query(request);
                        count = this.matches.size() + result.getCount();
                        return count;
                    }
                    default: {
                        throw new IllegalStateException("search type other than scan or query");
                    }
                }
            }
        }
    }

}

+1

@rafagan You can use the following resources to learn about AppSync:

Documentation :
https://docs.aws.amazon.com/appsync/latest/devguide/welcome.html
https://aws-amplify.github.io/docs/android/api

Github repo for the project:
https://github.com/awslabs/aws-mobile-appsync-sdk-android

Please feel free to submit a PR with the suggested changes for DynamoDB. We will have a look and merge the changes if it looks good. I would encourage you to submit a PR if you have the changes so that you can credit for it.

This issue has been automatically closed because of inactivity. Please open a new issue if are still encountering problems.

Was this page helpful?
0 / 5 - 0 ratings