Skip to content

Commit

Permalink
feature: adds support for Null and NotNull query params in repository…
Browse files Browse the repository at this point in the history
… methods (resolved gh-399)
  • Loading branch information
bsbodden committed Apr 8, 2024
1 parent ad463f8 commit a66e59b
Show file tree
Hide file tree
Showing 9 changed files with 478 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import java.util.AbstractMap.SimpleEntry;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

public class RediSearchQuery implements RepositoryQuery {
Expand Down Expand Up @@ -84,13 +85,14 @@ public class RediSearchQuery implements RepositoryQuery {

private final RedisModulesOperations<String> modulesOperations;

private boolean isANDQuery = false;
private final boolean isANDQuery;

private final BloomQueryExecutor bloomQueryExecutor;
private final CuckooQueryExecutor cuckooQueryExecutor;
private final AutoCompleteQueryExecutor autoCompleteQueryExecutor;
private final GsonBuilder gsonBuilder;
private Gson gson;
private boolean isNullParamQuery;

@SuppressWarnings("unchecked")
public RediSearchQuery(//
Expand Down Expand Up @@ -120,6 +122,10 @@ public RediSearchQuery(//
@SuppressWarnings("rawtypes")
Class[] params = queryMethod.getParameters().stream().map(Parameter::getType).toArray(Class[]::new);
hasLanguageParameter = Arrays.stream(params).anyMatch(c -> c.isAssignableFrom(SearchLanguage.class));
isANDQuery = QueryClause.hasContainingAllClause(queryMethod.getName());

String methodName = isANDQuery ? QueryClause.getPostProcessMethodName(queryMethod.getName())
: queryMethod.getName();

try {
java.lang.reflect.Method method = repoClass.getMethod(queryMethod.getName(), params);
Expand Down Expand Up @@ -210,31 +216,45 @@ public RediSearchQuery(//
} else if (queryMethod.getName().startsWith(AutoCompleteQueryExecutor.AUTOCOMPLETE_PREFIX)) {
this.type = RediSearchQueryType.AUTOCOMPLETE;
} else {
isANDQuery = QueryClause.hasContainingAllClause(queryMethod.getName());
PartTree pt = new PartTree(methodName, metadata.getDomainType());

String methodName = isANDQuery ? QueryClause.getPostProcessMethodName(queryMethod.getName())
: queryMethod.getName();
List<String> nullParamNames = new ArrayList<>();
List<String> notNullParamNames = new ArrayList<>();

PartTree pt = new PartTree(methodName, metadata.getDomainType());
processPartTree(pt);
pt.getParts().forEach(part -> {
if (part.getType() == Part.Type.IS_NULL) {
nullParamNames.add(part.getProperty().getSegment());
} else if (part.getType() == Part.Type.IS_NOT_NULL) {
notNullParamNames.add(part.getProperty().getSegment());
}
});

this.isNullParamQuery = !nullParamNames.isEmpty() || !notNullParamNames.isEmpty();
this.type = RediSearchQueryType.QUERY;
this.returnFields = new String[] {};
processPartTree(pt, nullParamNames, notNullParamNames);
}
} catch (NoSuchMethodException | SecurityException e) {
logger.debug(String.format("Could not resolved query method %s(%s): %s", queryMethod.getName(),
Arrays.toString(params), e.getMessage()));
}
}

private void processPartTree(PartTree pt) {
private void processPartTree(PartTree pt, List<String> nullParamNames, List<String> notNullParamNames) {
pt.stream().forEach(orPart -> {
List<Pair<String, QueryClause>> orPartParts = new ArrayList<>();
orPart.iterator().forEachRemaining(part -> {
PropertyPath propertyPath = part.getProperty();

List<PropertyPath> path = StreamSupport.stream(propertyPath.spliterator(), false).toList();
orPartParts.addAll(extractQueryFields(domainType, part, path));

String paramName = path.get(path.size() - 1).getSegment();
if (nullParamNames.contains(paramName)) {
orPartParts.add(Pair.of(paramName, QueryClause.IS_NULL));
} else if (notNullParamNames.contains(paramName)) {
orPartParts.add(Pair.of(paramName, QueryClause.IS_NOT_NULL));
} else {
orPartParts.addAll(extractQueryFields(domainType, part, path));
}
});
queryOrParts.add(orPartParts);
});
Expand Down Expand Up @@ -357,7 +377,7 @@ public Object execute(Object[] parameters) {
} else if (maybeCuckooFilter.isPresent()) {
return cuckooQueryExecutor.executeCuckooQuery(parameters, maybeCuckooFilter.get());
} else if (type == RediSearchQueryType.QUERY) {
return executeQuery(parameters);
return !isNullParamQuery ? executeQuery(parameters) : executeNullQuery(parameters);
} else if (type == RediSearchQueryType.AGGREGATION) {
return executeAggregation(parameters);
} else if (type == RediSearchQueryType.TAGVALS) {
Expand All @@ -378,7 +398,8 @@ public QueryMethod getQueryMethod() {

private Object executeQuery(Object[] parameters) {
SearchOperations<String> ops = modulesOperations.opsForSearch(searchIndex);
String preparedQuery = prepareQuery(parameters);
boolean excludeNullParams = !isNullParamQuery;
String preparedQuery = prepareQuery(parameters, excludeNullParams);
Query query = new Query(preparedQuery);
query.returnFields(returnFields);

Expand Down Expand Up @@ -587,7 +608,7 @@ private Object executeFtTagVals() {
return ops.tagVals(this.value);
}

private String prepareQuery(final Object[] parameters) {
private String prepareQuery(final Object[] parameters, boolean excludeNullParams) {
logger.debug(String.format("parameters: %s", Arrays.toString(parameters)));
List<Object> params = new ArrayList<>(Arrays.asList(parameters));
StringBuilder preparedQuery = new StringBuilder();
Expand All @@ -597,6 +618,9 @@ private String prepareQuery(final Object[] parameters) {
preparedQuery.append(queryOrParts.stream().map(qop -> {
String orPart = multipleOrParts ? "(" : "";
orPart = orPart + qop.stream().map(fieldClauses -> {
if (excludeNullParams && (fieldClauses.getSecond() == QueryClause.IS_NULL || fieldClauses.getSecond() == QueryClause.IS_NOT_NULL)) {
return "";
}
String fieldName = fieldClauses.getFirst();
QueryClause queryClause = fieldClauses.getSecond();
int paramsCnt = queryClause.getClauseTemplate().getNumberOfArguments();
Expand Down Expand Up @@ -645,6 +669,10 @@ private String prepareQuery(final Object[] parameters) {
}
}

if (preparedQuery.toString().isBlank()) {
preparedQuery.append("*");
}

logger.debug(String.format("query: %s", preparedQuery));

return preparedQuery.toString();
Expand All @@ -656,4 +684,89 @@ private Gson getGson() {
}
return gson;
}

private Object executeNullQuery(Object[] parameters) {
SearchOperations<String> ops = modulesOperations.opsForSearch(searchIndex);
String baseQuery = prepareQuery(parameters, true);

AggregationBuilder aggregation = new AggregationBuilder(baseQuery);

// Load fields with IS_NULL or IS_NOT_NULL query clauses
String[] fields = Stream.concat(Stream.of("@__key"), queryOrParts.stream().flatMap(List::stream)
.filter(pair -> pair.getSecond() == QueryClause.IS_NULL || pair.getSecond() == QueryClause.IS_NOT_NULL)
.map(pair -> String.format("@%s", pair.getFirst()))).toArray(String[]::new);

aggregation.load(fields);

// Apply exists or !exists filter for null parameters
for (List<Pair<String, QueryClause>> orPartParts : queryOrParts) {
for (Pair<String, QueryClause> pair : orPartParts) {
if (pair.getSecond() == QueryClause.IS_NULL) {
aggregation.filter("!exists(@" + pair.getFirst() + ")");
} else if (pair.getSecond() == QueryClause.IS_NOT_NULL) {
aggregation.filter("exists(@" + pair.getFirst() + ")");
}
}
}

// sort by
Optional<Pageable> maybePageable;

boolean needsLimit = true;
if (queryMethod.isPageQuery()) {
maybePageable = Arrays.stream(parameters).filter(Pageable.class::isInstance).map(Pageable.class::cast)
.findFirst();

if (maybePageable.isPresent()) {
Pageable pageable = maybePageable.get();
if (!pageable.isUnpaged()) {
aggregation.limit(Math.toIntExact(pageable.getOffset()), pageable.getPageSize());
needsLimit = false;

// sort by
pageable.getSort();
for (Order order : pageable.getSort()) {
if (order.isAscending()) {
aggregation.sortByAsc(order.getProperty());
} else {
aggregation.sortByDesc(order.getProperty());
}
}
}
}
}

if ((sortBy != null && !sortBy.isBlank())) {
aggregation.sortByAsc(sortBy);
} else if (!aggregationSortedFields.isEmpty()) {
if (aggregationSortByMax != null) {
aggregation.sortBy(aggregationSortByMax, aggregationSortedFields.toArray(new SortedField[] {}));
} else {
aggregation.sortBy(aggregationSortedFields.toArray(new SortedField[] {}));
}
}

// limit
if (needsLimit) {
if ((limit != null) || (offset != null)) {
aggregation.limit(offset != null ? offset : 0, limit != null ? limit : 0);
} else {
aggregation.limit(0, redisOMProperties.getRepository().getQuery().getLimit());
}
}

// Execute the aggregation query
AggregationResult aggregationResult = ops.aggregate(aggregation);

// extract the keys from the aggregation result
String[] keys = aggregationResult.getResults().stream().map(d -> d.get("__key").toString()).toArray(String[]::new);

var entities = modulesOperations.opsForJSON().mget(domainType, keys);

if (!queryMethod.isCollectionQuery()) {
return entities.isEmpty() ? null : entities.get(0);
} else {
return entities;
}
}
}

0 comments on commit a66e59b

Please sign in to comment.