DynamoDBMapper: запрос/сканирование с фильтром по атрибуту вложенного объекта

Я создаю класс DAO, у которого есть API для получения продуктов постранично. В запросе к API будет список фильтров. Фильтрация работает должным образом для примитивных и строковых атрибутов.

@Getter
@Setter
@DynamoDBTable(tableName = "Entity")
public abstract class EntityDO {

    @DynamoDBHashKey(attributeName = "uid")
    @DynamoDBAutoGeneratedKey
    private String uid;
}

@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@DynamoDBDocument
@DynamoDBTable(tableName = "CATEGORIES")
public class CategoryDO extends EntityDO {

    @DynamoDBAttribute(attributeName = "name")
    private String name;

    @DynamoDBAttribute(attributeName = "description")
    private String description;

    @DynamoDBAttribute(attributeName = "imageUrl")
    private String imageUrl;
}

@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@DynamoDBTable(tableName = "bs-PRODUCTS")
public class ProductDO extends EntityDO {

    @DynamoDBAttribute(attributeName = "name")
    private String name;

    @DynamoDBAttribute(attributeName = "description")
    private String description;

    @DynamoDBAttribute(attributeName = "longDescription")
    private String longDescription;

    @DynamoDBAttribute(attributeName = "category")
    private CategoryDO category;

    @DynamoDBAttribute(attributeName = "imageUrl")
    private String imageUrl;

    @DynamoDBAttribute(attributeName = "mrp")
    private float mrp;

    @DynamoDBAttribute(attributeName = "discount")
    private float discount;

    @DynamoDBAttribute(attributeName = "tags")
    private List<String> tags;
}


public class ProductDAO extends EntityDAO<ProductDO> {

    @Autowired
    private Logger logger;

    @Autowired
    private DynamoDBMapper dynamoDBMapper;

    @Autowired
    private Map<Filter.Operation, ComparisonOperator> operatorMap;

    @Autowired
    private ProductConverter converter;

    @Override
    public Optional<ProductDO> get(String id) {
        return Optional.ofNullable(dynamoDBMapper.load(ProductDO.class, id));
    }

    @Override
    public List<ProductDO> getAll() {
        return dynamoDBMapper.scan(ProductDO.class, new DynamoDBScanExpression());
    }

    @Override
    public List<ProductDO> get(List<Filter> filters, String previousPageLastKey, int count) {

        Map<String, Condition> scanFilters = new HashMap<>();
        for (Filter filter: filters) {
            ComparisonOperator comparisonOperator = operatorMap.get(filter.getOperation());
            Condition condition = new Condition()
                    .withComparisonOperator(comparisonOperator)
                    .withAttributeValueList(new AttributeValue().withS(filter.getAttributeValue()));
            scanFilters.put(filter.getAttributeName(), condition);
        }

        DynamoDBScanExpression scanExpression = new DynamoDBScanExpression()
                .withLimit(count)
                .withScanFilter(scanFilters);
        if (previousPageLastKey != null) {
            Map<String, AttributeValue> exclusiveStartKey = new HashMap<>();
            exclusiveStartKey.put(Constants.UID, new AttributeValue().withS(previousPageLastKey));
            scanExpression.setExclusiveStartKey(exclusiveStartKey);
        }

        return dynamoDBMapper.scan(ProductDO.class, scanExpression);
    }
}

Что нужно изменить в ScanExpression для фильтрации по category.uid?

Я попытался передать имя атрибута как category.uid, но не помогло.

Я готов узнать мнение критика, если подход неверен с точки зрения дизайна. Если есть несколько способов, вы можете подробно рассказать о плюсах и минусах.

Более того, я пробовал это на консоли AWS, и там это тоже не работает. Моя таблица выглядит следующим образом: Все продукты

Один из продуктов имеет содержимое, как показано ниже Проверьте поле категории, которое является картой

Проверьте поле категории, которое представляет собой карту, содержащую name и uid. Я попытался найти название категории, и никаких результатов не появилось. введите здесь описание изображения

Поддерживает ли AWS DynamoDB фильтрацию по вложенным атрибутам?


person Kuldeep Yadav    schedule 10.10.2018    source источник


Ответы (1)


Я попал в тупик со ScanFilters, в основном они не поддерживают вложенные атрибуты для поиска в документах Dynamodb (вложенный объект). Это старый способ фильтрации, и FilterExpressions заменяют его.

FilterExpressions также поддерживает вложенные атрибуты. Выражения фильтра в основном представляют собой строку, в которой есть заполнители для имен атрибутов и значений атрибутов. Можно не использовать заполнители, но в Dynamodb так много зарезервированных ключевых слов, что это рекомендуется. И эти заполнители предоставляются с помощью карт, expressionAttributeNames и expressionAttributeValues.

Выражение фильтра выглядит как #category.#uid = :categoryuid, где моя карта имен атрибутов выглядит как {"#category":"category","#uid":"uid"}, а карта значений атрибутов выглядит как {":categoryuid":{"s":"1d5e9cea-3c4d-4a73-8e1e-aeaa868b9d89"}}.

У меня это отлично сработало, для лучшей организации кода я записал класс-оболочку,

public class FilterExpression {

    private List<Filter> filters;

    private String filterExpression;

    private Map<String, AttributeValue> attributeValues;

    private Map<String, String> attributeNames;

    public FilterExpression(List<Filter> filters) {
        this.filters = filters;
        populateFilterExpression();
    }

    private void populateFilterExpression() {
        StringBuilder filterExpressionBuilder = new StringBuilder();
        attributeNames = new HashMap<>();
        attributeValues = new HashMap<>();

        for (Filter filter: filters) {
            if (filterExpressionBuilder.length() > 0) {
                filterExpressionBuilder.append(" AND ");
            }
            String attributeName = filter.getAttributeName();
            String[] attributes = attributeName.split("\\.");

            StringBuilder expNestedAttributes = new StringBuilder();
            for (String attributeInPath: attributes) {
                attributeNames.put("#"+attributeInPath, attributeInPath);
                if(expNestedAttributes.length() > 0) {
                    expNestedAttributes.append(".");
                }
                expNestedAttributes.append("#" + attributeInPath);
            }

            String attributeValueKey = ":" + String.join("", attributes);

            AttributeValue attributeValue;
            switch (filter.getAttributeType()) {
                case STRING:
                    attributeValue = new AttributeValue().withS(filter.getAttributeValue());
                    break;

                case NUMBER:
                    attributeValue = new AttributeValue().withN(filter.getAttributeValue());
                    break;

                default:
                    throw new UnsupportedOperationException("The attribute type is not supported");
            }
            attributeValues.put(attributeValueKey, attributeValue);

            switch (filter.getOperation()) {
                case EQ:
                    filterExpressionBuilder.append(expNestedAttributes);
                    filterExpressionBuilder.append(" = ");
                    filterExpressionBuilder.append(attributeValueKey);
                    break;

                case GE:
                    filterExpressionBuilder.append(expNestedAttributes);
                    filterExpressionBuilder.append(" >= ");
                    filterExpressionBuilder.append(attributeValueKey);
                    break;

                case LE:
                    filterExpressionBuilder.append(expNestedAttributes);
                    filterExpressionBuilder.append(" <= ");
                    filterExpressionBuilder.append(attributeValueKey);
                    break;

                case GT:
                    filterExpressionBuilder.append(expNestedAttributes);
                    filterExpressionBuilder.append(" > ");
                    filterExpressionBuilder.append(attributeValueKey);
                    break;

                case LT:
                    filterExpressionBuilder.append(expNestedAttributes);
                    filterExpressionBuilder.append(" < ");
                    filterExpressionBuilder.append(attributeValueKey);
                    break;

                case STARTS_WITH:
                    filterExpressionBuilder.append("begins_with (");
                    filterExpressionBuilder.append(expNestedAttributes);
                    filterExpressionBuilder.append(", ");
                    filterExpressionBuilder.append(attributeValueKey);
                    filterExpressionBuilder.append(")");
                    break;

                case CONTAINS:
                    filterExpressionBuilder.append("contains (");
                    filterExpressionBuilder.append(expNestedAttributes);
                    filterExpressionBuilder.append(", ");
                    filterExpressionBuilder.append(attributeValueKey);
                    filterExpressionBuilder.append(")");
                    break;

                default:
                    throw new UnsupportedOperationException("filter is not supported");
            }
        }

        filterExpression = filterExpressionBuilder.toString();
    }

    public String getFilterExpression() {
        return filterExpression;
    }

    public Map<String, AttributeValue> getAttributeValues() {
        return attributeValues;
    }

    public Map<String, String> getAttributeNames() {
        return attributeNames;
    }

    @Override
    public String toString() {
        return filterExpression;
    }
}

и мой Filter выглядит так:

public class Filter {

    private String attributeName;

    private String attributeValue;

    private Operation operation;

    private AttributeType attributeType;

    public enum Operation {
        EQ, GE, LE, GT, LT, CONTAINS, STARTS_WITH
    }

    public enum AttributeType {
        STRING, NUMBER
    }
}

фрагмент из DAO выглядит так

public List<ProductDO> get(List<Filter> filters, String previousPageLastKey, int count) {

    FilterExpression filterExpression = new FilterExpression(filters);
    DynamoDBScanExpression scanExpression = new DynamoDBScanExpression()
            .withLimit(count)
            .withFilterExpression(filterExpression.getFilterExpression())
            .withExpressionAttributeNames(filterExpression.getAttributeNames())
            .withExpressionAttributeValues(filterExpression.getAttributeValues());

    dynamoDBMapper.scan(ProductDO.class, scanExpression)
}

Разместив это, я не нашел подходящего решения по этому поводу.

person Kuldeep Yadav    schedule 13.10.2018
comment
Не могли бы вы поделиться своим кодом в git? Я не могу увидеть использование previousPageLastKey в методе get. - person Sanjari; 24.02.2019
comment
@Санджари, он тебе еще нужен? bitbucket.org/tarkshala/bluescooterapis/src/master - person Kuldeep Yadav; 24.02.2020