Add support for limiting keywords in derived query methods.

This commit is contained in:
Michael Simons
2019-05-20 11:02:00 +02:00
parent 49113379e2
commit ed962790ae
9 changed files with 94 additions and 13 deletions

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
/**

View File

@@ -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);

View File

@@ -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()

View File

@@ -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();

View File

@@ -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();
}

View File

@@ -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();
}