Skip to content

Commit

Permalink
FMWK-351 Find by nested CDT (#728)
Browse files Browse the repository at this point in the history
* add support for nested "findBy" queries (one level)
* add tests
  • Loading branch information
agrgr authored Apr 14, 2024
1 parent 38ba559 commit 0d491ed
Show file tree
Hide file tree
Showing 25 changed files with 1,976 additions and 351 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package org.springframework.data.aerospike.query.qualifier;

import com.aerospike.client.Value;
import com.aerospike.client.command.ParticleType;

import java.util.List;

import static org.springframework.data.aerospike.query.qualifier.QualifierKey.DOT_PATH;
import static org.springframework.data.aerospike.query.qualifier.QualifierKey.FIELD;
import static org.springframework.data.aerospike.query.qualifier.QualifierKey.IGNORE_CASE;
import static org.springframework.data.aerospike.query.qualifier.QualifierKey.KEY;
import static org.springframework.data.aerospike.query.qualifier.QualifierKey.NESTED_KEY;
import static org.springframework.data.aerospike.query.qualifier.QualifierKey.NESTED_TYPE;
import static org.springframework.data.aerospike.query.qualifier.QualifierKey.SECOND_VALUE;
import static org.springframework.data.aerospike.query.qualifier.QualifierKey.VALUE;

Expand Down Expand Up @@ -40,6 +43,18 @@ public QualifierBuilder setKey(Value key) {
return this;
}

/**
* For "find by one level nested map containing" queries.
* Set nested Map key.
* <p>
* Use one of the Value get() methods ({@link Value#get(int)}, {@link Value#get(String)} etc.) to firstly read the
* key into a {@link Value} object.
*/
public QualifierBuilder setNestedKey(Value key) {
this.map.put(NESTED_KEY, key);
return this;
}

/**
* Set value.
* <p>
Expand All @@ -62,6 +77,15 @@ public QualifierBuilder setSecondValue(Value secondValue) {
return this;
}

/**
* For "find by one level nested map containing" queries.
* Set the type of the nested map value using {@link ParticleType}.
*/
public QualifierBuilder setNestedType(int type) {
this.map.put(NESTED_TYPE, type);
return this;
}

/**
* Required only for a nested value query (e.g. find by a POJO field).
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ public enum QualifierKey {
MULTIPLE_IDS_FIELD,
IGNORE_CASE,
KEY,
NESTED_KEY,
VALUE,
SECOND_VALUE,
NESTED_TYPE,
DOT_PATH,
DATA_SETTINGS,
QUALIFIERS,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,22 @@ private IAerospikeQueryCreator getQueryCreator(Part part, AerospikePersistentPro
if (property.isIdProperty()) {
queryCreator = new IdQueryCreator(queryParameters);
} else if (property.isCollectionLike()) {
queryCreator = new CollectionQueryCreator(part, property, fieldName, queryParameters, filterOperation, converter);
if (part.getProperty().hasNext()) { // a POJO field
PropertyPath nestedProperty = getNestedPropertyPath(part.getProperty());
queryCreator = new CollectionQueryCreator(part, nestedProperty, property, fieldName, queryParameters,
filterOperation, converter, true);
} else {
queryCreator = new CollectionQueryCreator(part, part.getProperty(), property, fieldName,
queryParameters, filterOperation, converter, false);
}
} else if (property.isMap()) {
queryCreator = new MapQueryCreator(part, property, fieldName, queryParameters, filterOperation, converter);
if (part.getProperty().hasNext()) { // a POJO field
queryCreator = new MapQueryCreator(part, property, fieldName, queryParameters, filterOperation,
converter, true);
} else {
queryCreator = new MapQueryCreator(part, property, fieldName, queryParameters, filterOperation,
converter, false);
}
} else {
if (part.getProperty().hasNext()) { // a POJO field (a simple property field or an inner POJO)
PropertyPath nestedProperty = getNestedPropertyPath(part.getProperty());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
import org.springframework.data.util.TypeInformation;
import org.springframework.util.StringUtils;

import java.util.Collection;
import java.util.List;
import java.util.TreeMap;
import java.util.stream.Stream;

import static org.springframework.data.aerospike.convert.AerospikeConverter.CLASS_KEY;
import static org.springframework.data.aerospike.repository.query.CriteriaDefinition.AerospikeNullQueryCriterion;
Expand All @@ -24,8 +26,8 @@

public class AerospikeQueryCreatorUtils {

protected static Qualifier setQualifier(QualifierBuilder qb,
String fieldName, FilterOperation op, Part part, List<String> dotPath) {
protected static Qualifier setQualifier(QualifierBuilder qb, String fieldName, FilterOperation op, Part part,
List<String> dotPath) {
qb.setField(fieldName)
.setFilterOperation(op)
.setIgnoreCase(ignoreCaseToBoolean(part));
Expand Down Expand Up @@ -131,6 +133,10 @@ protected static void setQualifierBuilderKey(QualifierBuilder qb, Object key) {
qb.setKey(getValueOfQueryParameter(key));
}

protected static void setQualifierBuilderSecondKey(QualifierBuilder qb, Object key) {
qb.setNestedKey(getValueOfQueryParameter(key));
}

protected static void setQualifierBuilderValue(QualifierBuilder qb, Object value) {
qb.setValue(getValueOfQueryParameter(value));
}
Expand All @@ -153,27 +159,34 @@ protected static boolean isPojo(Class<?> clazz) { // if it is a first level POJO
return !Utils.isSimpleValueType(clazz) && !type.isCollectionLike();
}

protected static void validateTypes(MappingAerospikeConverter converter, PropertyPath property, FilterOperation op,
List<Object> queryParameters) {
String queryPartDescription = String.join(" ", property.toString(), op.toString());
validateTypes(converter, property, queryParameters, queryPartDescription);
protected static void validateTypes(MappingAerospikeConverter converter, PropertyPath propertyPath,
FilterOperation op, List<Object> queryParameters) {
String queryPartDescription = String.join(" ", propertyPath.toString(), op.toString());
validateTypes(converter, propertyPath, queryParameters, op, queryPartDescription);
}

protected static void validateTypes(MappingAerospikeConverter converter, PropertyPath property,
List<Object> queryParameters, String queryPartDescription) {
validateTypes(converter, property.getTypeInformation().getType(), queryParameters, queryPartDescription);
protected static void validateTypes(MappingAerospikeConverter converter, PropertyPath propertyPath,
List<Object> queryParameters, FilterOperation op, String queryPartDescription) {
validateTypes(converter, propertyPath.getTypeInformation()
.getType(), queryParameters, op, queryPartDescription);
}

protected static void validateTypes(MappingAerospikeConverter converter, Class<?> propertyType,
List<Object> queryParameters, String queryPartDescription,
List<Object> queryParameters, FilterOperation op, String queryPartDescription,
String... alternativeTypes) {
// Checking versus Number rather than strict type to be able to compare, e.g., integer to a long
if (isAssignable(Number.class, propertyType) && isAssignableValue(Number.class, queryParameters.get(0))) {
propertyType = Number.class;
}

Class<?> clazz = propertyType;
if (!queryParameters.stream().allMatch(param -> isAssignableValueOrConverted(clazz, param, converter))) {
Stream<Object> params = queryParameters.stream();
if ((op == FilterOperation.IN || op == FilterOperation.NOT_IN)
&& queryParameters.size() == 1
&& queryParameters.get(0) instanceof Collection<?>) {
params = ((Collection<Object>) queryParameters.get(0)).stream();
}
if (!params.allMatch(param -> isAssignableValueOrConverted(clazz, param, converter))) {
String validTypes = propertyType.getSimpleName();
if (alternativeTypes.length > 0) {
validTypes = String.format("one of the following types: %s", propertyType.getSimpleName() + ", "
Expand All @@ -184,6 +197,20 @@ protected static void validateTypes(MappingAerospikeConverter converter, Class<?
}
}

protected static void validateQueryIsNull(List<Object> queryParameters, String queryPartDescription) {
// Number of arguments is not zero
if (!queryParameters.isEmpty()) {
throw new IllegalArgumentException(queryPartDescription + ": expecting no arguments");
}
}

protected static void validateQueryIn(List<Object> queryParameters, String queryPartDescription) {
// Number of arguments is not one
if (queryParameters.size() != 1) {
throw new IllegalArgumentException(queryPartDescription + ": invalid number of arguments, expecting one");
}
}

protected static boolean isAssignableValueOrConverted(Class<?> propertyType, Object obj,
MappingAerospikeConverter converter) {
return isAssignableValue(propertyType, obj)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.springframework.data.aerospike.repository.query;

import com.aerospike.client.command.ParticleType;
import org.springframework.data.aerospike.convert.MappingAerospikeConverter;
import org.springframework.data.aerospike.mapping.AerospikePersistentProperty;
import org.springframework.data.aerospike.query.FilterOperation;
Expand All @@ -12,44 +13,57 @@
import java.util.List;

import static org.springframework.data.aerospike.query.FilterOperation.BETWEEN;
import static org.springframework.data.aerospike.query.FilterOperation.CONTAINING;
import static org.springframework.data.aerospike.query.FilterOperation.IN;
import static org.springframework.data.aerospike.query.FilterOperation.IS_NOT_NULL;
import static org.springframework.data.aerospike.query.FilterOperation.IS_NULL;
import static org.springframework.data.aerospike.query.FilterOperation.NOT_CONTAINING;
import static org.springframework.data.aerospike.query.FilterOperation.NOT_IN;
import static org.springframework.data.aerospike.repository.query.AerospikeQueryCreatorUtils.getCollectionElementsClass;
import static org.springframework.data.aerospike.repository.query.AerospikeQueryCreatorUtils.getCorrespondingMapValueFilterOperationOrFail;
import static org.springframework.data.aerospike.repository.query.AerospikeQueryCreatorUtils.setQualifier;
import static org.springframework.data.aerospike.repository.query.AerospikeQueryCreatorUtils.setQualifierBuilderKey;
import static org.springframework.data.aerospike.repository.query.AerospikeQueryCreatorUtils.setQualifierBuilderSecondValue;
import static org.springframework.data.aerospike.repository.query.AerospikeQueryCreatorUtils.setQualifierBuilderValue;
import static org.springframework.data.aerospike.repository.query.AerospikeQueryCreatorUtils.validateQueryIn;
import static org.springframework.data.aerospike.repository.query.AerospikeQueryCreatorUtils.validateQueryIsNull;
import static org.springframework.data.aerospike.repository.query.AerospikeQueryCreatorUtils.validateTypes;

public class CollectionQueryCreator implements IAerospikeQueryCreator {

private final Part part;
private final PropertyPath propertyPath;
private final AerospikePersistentProperty property;
private final String fieldName;
private final List<Object> queryParameters;
private final FilterOperation filterOperation;
private final MappingAerospikeConverter converter;
private final boolean isNested;

public CollectionQueryCreator(Part part, AerospikePersistentProperty property, String fieldName,
List<Object> queryParameters, FilterOperation filterOperation,
MappingAerospikeConverter converter) {
public CollectionQueryCreator(Part part, PropertyPath propertyPath, AerospikePersistentProperty property,
String fieldName, List<Object> queryParameters, FilterOperation filterOperation,
MappingAerospikeConverter converter, boolean isNested) {
this.part = part;
this.propertyPath = propertyPath;
this.property = property;
this.fieldName = fieldName;
this.queryParameters = queryParameters;
this.filterOperation = filterOperation;
this.converter = converter;
this.isNested = isNested;
}

@Override
public void validate() {
String queryPartDescription = String.join(" ", part.getProperty().toString(), filterOperation.toString());
String queryPartDescription = String.join(" ", propertyPath.toString(), filterOperation.toString());
switch (filterOperation) {
case CONTAINING, NOT_CONTAINING -> validateCollectionQueryContaining(queryParameters, queryPartDescription);
case EQ, NOTEQ, GT, GTEQ, LT, LTEQ -> validateCollectionQueryComparison(queryParameters,
queryPartDescription);
case BETWEEN -> validateCollectionQueryBetween(queryParameters, queryPartDescription);
default -> throw new UnsupportedOperationException(
String.format("Unsupported operation: %s applied to %s", filterOperation, property));
case IN, NOT_IN -> validateQueryIn(queryParameters, queryPartDescription);
case IS_NOT_NULL, IS_NULL -> validateQueryIsNull(queryParameters, queryPartDescription);
default -> throw new UnsupportedOperationException("Unsupported operation: " + queryPartDescription);
}
}

Expand All @@ -69,7 +83,7 @@ private void validateCollectionQueryComparison(List<Object> queryParameters, Str
}

if (queryParameters.get(0) instanceof Collection) {
validateTypes(converter, Collection.class, queryParameters, queryPartDescription);
validateTypes(converter, Collection.class, queryParameters, filterOperation, queryPartDescription);
} else {
throw new IllegalArgumentException(queryPartDescription + ": invalid argument type, expecting Collection");
}
Expand All @@ -84,7 +98,7 @@ private void validateCollectionQueryBetween(List<Object> queryParameters, String
// Not Collection
Object value = queryParameters.get(0);
if (value instanceof Collection) {
validateTypes(converter, Collection.class, queryParameters, queryPartDescription);
validateTypes(converter, Collection.class, queryParameters, filterOperation, queryPartDescription);
} else {
throw new IllegalArgumentException(queryPartDescription + ": invalid argument type, expecting Collection");
}
Expand All @@ -100,13 +114,14 @@ private void validateCollectionContainingTypes(PropertyPath property, List<Objec
String queryPartDescription) {
Object value = queryParameters.get(0);
if (value instanceof Collection) {
validateTypes(converter, Collection.class, queryParameters, queryPartDescription);
validateTypes(converter, Collection.class, queryParameters, filterOperation, queryPartDescription);
} else if (!(value instanceof CriteriaDefinition.AerospikeNullQueryCriterion)) {
// Not null param
// Determining class of Collection's elements
Class<?> componentsClass = getCollectionElementsClass(property);
if (componentsClass != null) {
validateTypes(converter, componentsClass, queryParameters, queryPartDescription, "Collection");
validateTypes(converter, componentsClass, queryParameters, filterOperation, queryPartDescription,
"Collection");
}
}
}
Expand All @@ -118,16 +133,33 @@ public Qualifier process() {

if (filterOperation == BETWEEN || filterOperation == IN || filterOperation == NOT_IN) {
setQualifierBuilderValue(qb, queryParameters.get(0));
if (queryParameters.size() >= 2) setQualifierBuilderSecondValue(qb, queryParameters.get(1));
if (queryParameters.size() == 2) setQualifierBuilderSecondValue(qb, queryParameters.get(1));
}

if (!(queryParameters.get(0) instanceof Collection<?>)) {
// CONTAINING
op = getCorrespondingListFilterOperationOrFail(op);
List<String> dotPath = null;
if (isNested) { // POJO field
if (op == CONTAINING || op == NOT_CONTAINING) {
qb.setNestedType(ParticleType.LIST);
}

// getting MAP_VAL_ operation because the property is in a POJO which is represented by a Map in DB
op = getCorrespondingMapValueFilterOperationOrFail(op);

if (queryParameters.isEmpty() && (filterOperation == IS_NOT_NULL || filterOperation == IS_NULL)) {
setQualifierBuilderValue(qb, property.getFieldName());
} else {
setQualifierBuilderValue(qb, queryParameters.get(0));
setQualifierBuilderKey(qb, property.getFieldName());
}
dotPath = List.of(part.getProperty().toDotPath());
} else { // first level
if (op == CONTAINING || op == NOT_CONTAINING) {
op = getCorrespondingListFilterOperationOrFail(op);
}
setQualifierBuilderValue(qb, queryParameters.get(0));
}
setQualifierBuilderValue(qb, queryParameters.get(0));

return setQualifier(qb, fieldName, op, part, null);
return setQualifier(qb, fieldName, op, part, dotPath);
}

private FilterOperation getCorrespondingListFilterOperationOrFail(FilterOperation op) {
Expand Down
Loading

0 comments on commit 0d491ed

Please sign in to comment.