Add support for limiting keywords in derived query methods.
This commit is contained in:
@@ -27,6 +27,7 @@ import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatch;
|
||||
import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatchWithWhere;
|
||||
import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatchWithoutWhere;
|
||||
import org.springframework.data.neo4j.core.cypher.support.Visitable;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@@ -225,14 +226,20 @@ class DefaultStatementBuilder
|
||||
}
|
||||
|
||||
@Override
|
||||
public final OngoingMatchAndReturn skip(Number number) {
|
||||
skip = Skip.of(number);
|
||||
public final OngoingMatchAndReturn skip(@Nullable Number number) {
|
||||
|
||||
if (number != null) {
|
||||
skip = Skip.create(number);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final OngoingMatchAndReturn limit(Number number) {
|
||||
limit = Limit.of(number);
|
||||
public final OngoingMatchAndReturn limit(@Nullable Number number) {
|
||||
|
||||
if (number != null) {
|
||||
limit = Limit.create(number);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ package org.springframework.data.neo4j.core.cypher;
|
||||
import org.apiguardian.api.API;
|
||||
import org.springframework.data.neo4j.core.cypher.support.Visitable;
|
||||
import org.springframework.data.neo4j.core.cypher.support.Visitor;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* @author Gerrit Meier
|
||||
@@ -30,7 +31,10 @@ import org.springframework.data.neo4j.core.cypher.support.Visitor;
|
||||
@API(status = API.Status.INTERNAL, since = "1.0")
|
||||
public final class Limit implements Visitable {
|
||||
|
||||
public static Limit of(Number value) {
|
||||
static Limit create(Number value) {
|
||||
|
||||
Assert.notNull(value, "A limit cannot have a null value.");
|
||||
|
||||
return new Limit(new NumberLiteral(value));
|
||||
}
|
||||
|
||||
@@ -44,5 +48,6 @@ public final class Limit implements Visitable {
|
||||
public void accept(Visitor visitor) {
|
||||
visitor.enter(this);
|
||||
limitAmount.accept(visitor);
|
||||
visitor.leave(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ package org.springframework.data.neo4j.core.cypher;
|
||||
import org.apiguardian.api.API;
|
||||
import org.springframework.data.neo4j.core.cypher.support.Visitable;
|
||||
import org.springframework.data.neo4j.core.cypher.support.Visitor;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* @author Gerrit Meier
|
||||
@@ -30,7 +31,10 @@ import org.springframework.data.neo4j.core.cypher.support.Visitor;
|
||||
@API(status = API.Status.INTERNAL, since = "1.0")
|
||||
public final class Skip implements Visitable {
|
||||
|
||||
public static Skip of(Number value) {
|
||||
static Skip create(Number value) {
|
||||
|
||||
Assert.notNull(value, "Cannot skip a value of null.");
|
||||
|
||||
return new Skip(new NumberLiteral(value));
|
||||
}
|
||||
|
||||
@@ -44,5 +48,6 @@ public final class Skip implements Visitable {
|
||||
public void accept(Visitor visitor) {
|
||||
visitor.enter(this);
|
||||
skipAmount.accept(visitor);
|
||||
visitor.leave(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
package org.springframework.data.neo4j.core.cypher;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* @author Michael J. Simons
|
||||
@@ -236,10 +237,10 @@ public interface StatementBuilder {
|
||||
/**
|
||||
* Adds a skip clause, skipping the given number of records.
|
||||
*
|
||||
* @param number How many records to skip
|
||||
* @return A step that only allows the limit of records to be specified
|
||||
* @param number How many records to skip. If this is null, then no records are skipped.
|
||||
* @return A step that only allows the limit of records to be specified.
|
||||
*/
|
||||
ExposesLimit skip(Number number);
|
||||
ExposesLimit skip(@Nullable Number number);
|
||||
|
||||
}
|
||||
|
||||
@@ -250,10 +251,10 @@ public interface StatementBuilder {
|
||||
|
||||
/**
|
||||
* Limits the number of returned records.
|
||||
* @param number How many records to return
|
||||
* @return A buildable match statement
|
||||
* @param number How many records to return. If this is null, all the records are returned.
|
||||
* @return A buildable match statement.
|
||||
*/
|
||||
BuildableMatch limit(Number number);
|
||||
BuildableMatch limit(@Nullable Number number);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -79,6 +79,11 @@ final class CypherQueryCreator extends AbstractQueryCreator<String, Condition> {
|
||||
private Iterator<Neo4jParameter> formalParameters;
|
||||
private final Queue<Parameter> lastParameter = new LinkedList<>();
|
||||
|
||||
/**
|
||||
* Stores the number of max results, if the {@link PartTree tree} is limiting.
|
||||
*/
|
||||
private final Number maxResults;
|
||||
|
||||
/**
|
||||
* Sort items may already be needed for some parts, i.e. of type NEAR.
|
||||
*/
|
||||
@@ -93,6 +98,7 @@ final class CypherQueryCreator extends AbstractQueryCreator<String, Condition> {
|
||||
this.domainType = domainType;
|
||||
this.nodeDescription = this.mappingContext.getRequiredNodeDescription(this.domainType);
|
||||
this.formalParameters = formalParameters.iterator();
|
||||
this.maxResults = tree.isLimiting() ? tree.getMaxResults() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -127,6 +133,7 @@ final class CypherQueryCreator extends AbstractQueryCreator<String, Condition> {
|
||||
sort.stream().map(sortAdapterFor(nodeDescription))
|
||||
).toArray(SortItem[]::new)
|
||||
)
|
||||
.limit(maxResults)
|
||||
.build();
|
||||
|
||||
return CypherRenderer.create().render(statement);
|
||||
|
||||
@@ -105,6 +105,7 @@ final class PartTreeNeo4jQuery extends AbstractNeo4jQuery {
|
||||
mappingContext, domainType, tree, formalParameters, actualParameters
|
||||
);
|
||||
|
||||
|
||||
String cypherQuery = queryCreator.createQuery();
|
||||
Map<String, Object> boundedParameters = formalParameters
|
||||
.getBindableParameters().stream()
|
||||
|
||||
@@ -241,6 +241,15 @@ public class CypherTest {
|
||||
"MATCH (u:`User`) RETURN u SKIP 1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void nullSkip() {
|
||||
Statement statement = Cypher.match(userNode).returning(userNode).skip(null).build();
|
||||
|
||||
assertThat(cypherRenderer.render(statement))
|
||||
.isEqualTo(
|
||||
"MATCH (u:`User`) RETURN u");
|
||||
}
|
||||
|
||||
@Test
|
||||
void limit() {
|
||||
Statement statement = Cypher.match(userNode).returning(userNode).limit(1).build();
|
||||
@@ -250,6 +259,15 @@ public class CypherTest {
|
||||
"MATCH (u:`User`) RETURN u LIMIT 1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void nullLimit() {
|
||||
Statement statement = Cypher.match(userNode).returning(userNode).limit(null).build();
|
||||
|
||||
assertThat(cypherRenderer.render(statement))
|
||||
.isEqualTo(
|
||||
"MATCH (u:`User`) RETURN u");
|
||||
}
|
||||
|
||||
@Test
|
||||
void skipAndLimit() {
|
||||
Statement statement = Cypher.match(userNode).returning(userNode).skip(1).limit(1).build();
|
||||
@@ -259,6 +277,16 @@ public class CypherTest {
|
||||
"MATCH (u:`User`) RETURN u SKIP 1 LIMIT 1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void nullskipAndLimit() {
|
||||
Statement statement = Cypher.match(userNode).returning(userNode).skip(null).limit(null).build();
|
||||
|
||||
assertThat(cypherRenderer.render(statement))
|
||||
.isEqualTo(
|
||||
"MATCH (u:`User`) RETURN u");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void distinct() {
|
||||
Statement statement = Cypher.match(userNode).returningDistinct(userNode).skip(1).limit(1).build();
|
||||
|
||||
@@ -26,6 +26,7 @@ import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import org.assertj.core.groups.Tuple;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -125,6 +126,10 @@ class RepositoryIT {
|
||||
transaction.run("CREATE (n:PersonWithWither) SET n.name = '" + TEST_PERSON1_NAME + "'");
|
||||
transaction.run("CREATE (n:KotlinPerson) SET n.name = '" + TEST_PERSON1_NAME + "'");
|
||||
transaction.run("CREATE (a:Thing {theId: 'anId', name: 'Homer'})");
|
||||
|
||||
IntStream.rangeClosed(1, 20).forEach(i ->
|
||||
transaction.run("CREATE (a:Thing {theId: 'id' + $i, name: 'name' + $i})", Values.parameters("i", String.format("%02d", i))));
|
||||
|
||||
transaction.success();
|
||||
transaction.close();
|
||||
|
||||
@@ -784,13 +789,30 @@ class RepositoryIT {
|
||||
.containsExactly(person1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void limitClauseShouldWork() {
|
||||
|
||||
List<ThingWithAssignedId> things;
|
||||
|
||||
things = thingRepository.findTop5ByOrderByNameDesc();
|
||||
assertThat(things)
|
||||
.hasSize(5)
|
||||
.extracting(ThingWithAssignedId::getName)
|
||||
.containsExactlyInAnyOrder("name20", "name19", "name18", "name17", "name16");
|
||||
|
||||
things = thingRepository.findFirstByOrderByNameDesc();
|
||||
assertThat(things)
|
||||
.extracting(ThingWithAssignedId::getName)
|
||||
.containsExactlyInAnyOrder("name20");
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableNeo4jRepositories
|
||||
@EnableTransactionManagement
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
public Driver driver() {
|
||||
public static Driver driver() {
|
||||
|
||||
return neo4jConnectionSupport.openConnection();
|
||||
}
|
||||
|
||||
@@ -18,10 +18,15 @@
|
||||
*/
|
||||
package org.springframework.data.neo4j.integration;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
|
||||
/**
|
||||
* @author Michael J. Simons
|
||||
*/
|
||||
public interface ThingRepository extends CrudRepository<ThingWithAssignedId, String> {
|
||||
List<ThingWithAssignedId> findFirstByOrderByNameDesc();
|
||||
|
||||
List<ThingWithAssignedId> findTop5ByOrderByNameDesc();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user