GH-2681 - Add anyOf / allOf to SpEL.
Currently we only have the literals clause to render an arbitrary string into a custom Cypher statement.
Although one could prepare the string upfront manually and joining multiple strings on | or &
and pass this to the query method, it would be more convenient to add one or two functions directly.
Namely those functions will be
anyOf : renders input ["a","b", ..] to A|B|..
allOf : renders input ["a","b", ..] to A&B&..
The usage would be MATCH (n::#{allOf(#labels)}) RETURN n, for example.
Closes #2681
This commit is contained in:
@@ -420,11 +420,16 @@ public interface MyPersonRepository extends Neo4jRepository<Person, Long> {
|
||||
<.> A `Pageable` has always the name `pageable` inside the SpEL context.
|
||||
<.> A `Sort` has always the name `sort` inside the SpEL context.
|
||||
|
||||
[[spel-extensions]]
|
||||
=== Spring Expression Language extensions
|
||||
|
||||
[[literal-extension]]
|
||||
==== Literal extension
|
||||
|
||||
The `literal` extension can be used to make things like labels or relationship-types "dynamic" in custom queries.
|
||||
Neither labels nor relationship types can be parameterized in Cypher, so they must be given literal.
|
||||
|
||||
[source,java]
|
||||
[[literal-extension]]
|
||||
.literal-Extension
|
||||
----
|
||||
interface BaseClassRepository extends Neo4jRepository<Inheritance.BaseClass, Long> {
|
||||
@@ -440,6 +445,25 @@ If you pass in `SomeLabel` as a parameter to the method, `MATCH (n:``SomeLabel``
|
||||
will be generated. Ticks have been added to correctly escape values. SDN won't do this
|
||||
for you as this is probably not what you want in all cases.
|
||||
|
||||
[[list-extensions]]
|
||||
==== List extensions
|
||||
|
||||
For more than one value there are `allOf` and `anyOf` in place that would render
|
||||
either a `&` or `|` concatenated list of all values.
|
||||
|
||||
[source,java]
|
||||
.List extensions
|
||||
----
|
||||
interface BaseClassRepository extends Neo4jRepository<Inheritance.BaseClass, Long> {
|
||||
|
||||
@Query("MATCH (n:`:#{allOf(#label)}`) RETURN n")
|
||||
List<Inheritance.BaseClass> findByLabels(List<String> labels);
|
||||
|
||||
@Query("MATCH (n:`:#{anyOf(#label)}`) RETURN n")
|
||||
List<Inheritance.BaseClass> findByLabels(List<String> labels);
|
||||
}
|
||||
----
|
||||
|
||||
=== Referring to Labels
|
||||
|
||||
You already know how to map a Node to a domain object:
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package org.springframework.data.neo4j.repository.query;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
@@ -23,7 +24,9 @@ import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.apiguardian.api.API;
|
||||
import org.springframework.core.log.LogAccessor;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.neo4j.core.mapping.CypherGenerator;
|
||||
@@ -49,8 +52,12 @@ import org.springframework.util.Assert;
|
||||
public final class Neo4jSpelSupport {
|
||||
|
||||
public static String FUNCTION_LITERAL = "literal";
|
||||
public static String FUNCTION_ANY_OF = "anyOf";
|
||||
public static String FUNCTION_ALL_OF = "allOf";
|
||||
public static String FUNCTION_ORDER_BY = "orderBy";
|
||||
|
||||
private static final LogAccessor LOG = new LogAccessor(LogFactory.getLog(Neo4jSpelSupport.class));
|
||||
|
||||
/**
|
||||
* Takes {@code arg} and tries to either extract a {@link Sort sort} from it or cast it to a sort. That sort is
|
||||
* than past to the {@link CypherGenerator} that renders a valid order by fragment which replaces the SpEL placeholder
|
||||
@@ -87,6 +94,34 @@ public final class Neo4jSpelSupport {
|
||||
return literalReplacement;
|
||||
}
|
||||
|
||||
public static LiteralReplacement anyOf(@Nullable Object arg) {
|
||||
return labels(arg, "|");
|
||||
}
|
||||
|
||||
public static LiteralReplacement allOf(@Nullable Object arg) {
|
||||
return labels(arg, "&");
|
||||
}
|
||||
|
||||
private static LiteralReplacement labels(@Nullable Object arg, String joinOn) {
|
||||
return StringBasedLiteralReplacement
|
||||
.withTargetAndValue(LiteralReplacement.Target.UNSPECIFIED,
|
||||
arg == null ? "" : joinStrings(arg, joinOn)
|
||||
);
|
||||
}
|
||||
|
||||
private static String joinStrings(Object arg, String joinOn) {
|
||||
if (arg instanceof Collection) {
|
||||
return ((Collection<?>) arg).stream().map(Object::toString).collect(Collectors.joining(joinOn));
|
||||
}
|
||||
|
||||
// we are so kind and also accept plain strings instead of collection<string>
|
||||
if (arg instanceof String) {
|
||||
return (String) arg;
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Cannot process argument %s. Please note that only Collection<String> and String are supported types.", arg));
|
||||
}
|
||||
/**
|
||||
* A marker interface that indicates a literal replacement in a query instead of a parameter replacement. This
|
||||
* comes in handy in places where non-parameterizable things should be created dynamic, for example matching on
|
||||
|
||||
@@ -45,12 +45,15 @@ public final class Neo4jEvaluationContextExtension implements EvaluationContextE
|
||||
|
||||
@Override
|
||||
public Map<String, Function> getFunctions() {
|
||||
|
||||
Map<String, Function> functions = new HashMap<>();
|
||||
functions.put(Neo4jSpelSupport.FUNCTION_ORDER_BY, new Function(ReflectionUtils
|
||||
.findRequiredMethod(Neo4jSpelSupport.class, Neo4jSpelSupport.FUNCTION_ORDER_BY, Object.class)));
|
||||
functions.put(Neo4jSpelSupport.FUNCTION_LITERAL, new Function(ReflectionUtils
|
||||
.findRequiredMethod(Neo4jSpelSupport.class, Neo4jSpelSupport.FUNCTION_LITERAL, Object.class)));
|
||||
functions.put(Neo4jSpelSupport.FUNCTION_ANY_OF, new Function(ReflectionUtils
|
||||
.findRequiredMethod(Neo4jSpelSupport.class, Neo4jSpelSupport.FUNCTION_ANY_OF, Object.class)));
|
||||
functions.put(Neo4jSpelSupport.FUNCTION_ALL_OF, new Function(ReflectionUtils
|
||||
.findRequiredMethod(Neo4jSpelSupport.class, Neo4jSpelSupport.FUNCTION_ALL_OF, Object.class)));
|
||||
|
||||
return functions;
|
||||
}
|
||||
|
||||
@@ -3842,6 +3842,29 @@ class RepositoryIT {
|
||||
.first().isInstanceOf(Inheritance.ConcreteClassB.class)
|
||||
.extracting(Inheritance.BaseClass::getName)
|
||||
.isEqualTo("cc2");
|
||||
|
||||
List<String> labels = new ArrayList<>();
|
||||
labels.add("ConcreteClassA");
|
||||
labels.add("ConcreteClassB");
|
||||
|
||||
assertThat(baseClassRepository.findByOrLabels(labels)).hasSize(2)
|
||||
.hasOnlyElementsOfTypes(Inheritance.ConcreteClassA.class, Inheritance.ConcreteClassB.class)
|
||||
.extracting(Inheritance.BaseClass::getName)
|
||||
.containsExactlyInAnyOrder("cc1", "cc2");
|
||||
|
||||
assertThat(baseClassRepository.findByAndLabels(labels)).hasSize(0);
|
||||
|
||||
String labelsString = "ConcreteClassA";
|
||||
assertThat(baseClassRepository.findByAndLabels(labelsString)).hasSize(1)
|
||||
.first().isInstanceOf(Inheritance.ConcreteClassA.class)
|
||||
.extracting(Inheritance.BaseClass::getName)
|
||||
.isEqualTo("cc1");
|
||||
|
||||
assertThatExceptionOfType(RuntimeException.class).isThrownBy(() -> baseClassRepository.findByAndLabels(1))
|
||||
.havingRootCause()
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.withMessageContaining("Cannot process argument");
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -4485,6 +4508,12 @@ class RepositoryIT {
|
||||
|
||||
@Query("MATCH (n::#{literal(#label)}) RETURN n")
|
||||
List<Inheritance.BaseClass> findByLabel(@Param("label") String label);
|
||||
|
||||
@Query("MATCH (n::#{anyOf(#label)}) RETURN n")
|
||||
List<Inheritance.BaseClass> findByOrLabels(@Param("label") List<String> labels);
|
||||
|
||||
@Query("MATCH (n::#{allOf(#label)}) RETURN n")
|
||||
List<Inheritance.BaseClass> findByAndLabels(@Param("label") Object labels);
|
||||
}
|
||||
|
||||
interface SuperBaseClassRepository extends Neo4jRepository<Inheritance.SuperBaseClass, Long> {
|
||||
|
||||
Reference in New Issue
Block a user