diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/annotation/Query.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/annotation/Query.java index ac935a0e3..aade74435 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/annotation/Query.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/annotation/Query.java @@ -40,6 +40,11 @@ public @interface Query { */ String value() default ""; + /** + * @return simpler count-query to be executed for @{see Pageable}-support {self} will be provided by the node-id of the current entity other parameters (e.g. {name}) by the given named params + */ + String countQuery() default ""; + /** * @return target type to convert the single result column (if any) to. */ @@ -52,4 +57,14 @@ public @interface Query { // FQN is a fix for javac compiler bug org.springframework.data.neo4j.annotation.QueryType type() default org.springframework.data.neo4j.annotation.QueryType.Cypher; + + /** + * @return name of the named query to be used for this annotated method, instead of Class.method + */ + String queryName() default ""; + + /** + * @return name of the named count query to be used for this annotated method, instead of Class.method.count + */ + String countQueryName() default ""; } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/AbstractGraphRepository.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/AbstractGraphRepository.java index 5da034109..fe06e3188 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/AbstractGraphRepository.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/AbstractGraphRepository.java @@ -37,6 +37,7 @@ import org.springframework.data.neo4j.mapping.Neo4jPersistentProperty; import org.springframework.data.neo4j.support.Neo4jTemplate; import org.springframework.data.neo4j.support.index.NoSuchIndexException; import org.springframework.data.neo4j.support.index.NullReadableIndex; +import org.springframework.data.neo4j.support.query.QueryEngine; import java.util.*; @@ -347,6 +348,11 @@ public abstract class AbstractGraphRepository im return findAll(); // todo } + @Override + public EndResult query(String query, Map params) { + return template.query(query, params).to(clazz); + } + @Override public Page findAll(final Pageable pageable) { int count = pageable.getPageSize(); @@ -442,9 +448,21 @@ public abstract class AbstractGraphRepository im @SuppressWarnings("unchecked") @Override - public Page query(Execute query, Map params, Pageable page) { + public Page query(Execute query, Execute countQuery, Map params, Pageable page) { final Execute limitedQuery = ((Skip)query).skip(page.getOffset()).limit(page.getPageSize()); - return template.queryEngineFor(QueryType.Cypher).query(limitedQuery.toString(), params).to(clazz).as(Page.class); + QueryEngine engine = template.queryEngineFor(QueryType.Cypher); + Page result = engine.query(limitedQuery.toString(), params).to(clazz).as(Page.class); + if (countQuery==null || result.getNumberOfElements() < page.getPageSize()) { + return result; + } + Long count = engine.query(countQuery.toString(), params).to(Long.class).singleOrNull(); + if (count==null) return result; + return new PageImpl(result.getContent(),page, count); + } + @SuppressWarnings("unchecked") + @Override + public Page query(Execute query, Map params, Pageable page) { + return query(query, null, params, page); } @SuppressWarnings("unchecked") diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/CRUDRepository.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/CRUDRepository.java index 507761185..b602a3e1d 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/CRUDRepository.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/CRUDRepository.java @@ -26,6 +26,8 @@ import org.springframework.data.repository.NoRepositoryBean; import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.transaction.annotation.Transactional; +import java.util.Map; + /** * CRUD interface for graph repositories, used as base repository for crud operations */ @@ -125,4 +127,6 @@ public interface CRUDRepository extends PagingAndSortingRepository { Class getStoredJavaType(Object entity); + EndResult query(String query, Map params); + } \ No newline at end of file diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/CypherDslRepository.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/CypherDslRepository.java index 4f36b62b9..1c7a26db1 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/CypherDslRepository.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/CypherDslRepository.java @@ -28,5 +28,6 @@ import java.util.Map; */ public interface CypherDslRepository { Page query(Execute query, Map params, Pageable page); + Page query(Execute query, Execute countQuery, Map params, Pageable page); EndResult query(Execute query, Map params); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/GraphQueryMethod.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/GraphQueryMethod.java index 00f01e0bd..e7120dca6 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/GraphQueryMethod.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/GraphQueryMethod.java @@ -25,6 +25,7 @@ import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.util.StringUtils; import java.lang.reflect.Method; import java.util.*; @@ -49,21 +50,36 @@ public class GraphQueryMethod extends QueryMethod { } public String getQueryString() { - return queryAnnotation != null ? queryAnnotation.value() : getNamedQuery(); + return queryAnnotation != null ? queryAnnotation.value() : getNamedQuery(getNamedQueryName()); + } + + @Override + public String getNamedQueryName() { + String annotatedName = queryAnnotation != null ? queryAnnotation.queryName() : null; + return StringUtils.hasText(annotatedName) ? annotatedName : super.getNamedQueryName(); + } + + public String getCountQueryString() { + return queryAnnotation != null ? queryAnnotation.countQuery() : getNamedQuery(getNamedCountQueryName()); } public boolean isValid() { return this.getQueryString() != null; // && this.compoundType != null } - private String getNamedQuery() { - final String namedQueryName = getNamedQueryName(); + private String getNamedQuery(String namedQueryName) { if (namedQueries.hasQuery(namedQueryName)) { return namedQueries.getQuery(namedQueryName); } return null; } + String getNamedCountQueryName() { + String annotatedName = queryAnnotation != null ? queryAnnotation.countQueryName() : null; + return StringUtils.hasText(annotatedName) ? annotatedName : getNamedQueryName() + ".count"; + } + + public Class getReturnType() { return method.getReturnType(); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/GraphRepositoryQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/GraphRepositoryQuery.java index 30763665c..db40d085d 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/GraphRepositoryQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/GraphRepositoryQuery.java @@ -28,6 +28,7 @@ import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; import java.util.*; @@ -95,7 +96,8 @@ abstract class GraphRepositoryQuery implements RepositoryQuery, ParameterResolve final Class compoundType = queryMethod.getCompoundType(); if (queryMethod.isPageQuery()) { @SuppressWarnings("unchecked") final Iterable result = queryEngine.query(queryString, params).to(compoundType); - return createPage(result, accessor.getPageable()); + Long count = computeCount(params); + return createPage(result, accessor.getPageable(),count); } if (queryMethod.isIterableResult()) { final EndResult result = queryEngine.query(queryString, params).to(compoundType); @@ -106,6 +108,12 @@ abstract class GraphRepositoryQuery implements RepositoryQuery, ParameterResolve return queryEngine.query(queryString, params).to(queryMethod.getReturnType()).singleOrNull(); } + private Long computeCount(Map params) { + String countQuery = queryMethod.getCountQueryString(); + if (countQuery == null || !StringUtils.hasText(countQuery)) return null; + return getQueryEngine().query(countQuery,params).to(Long.class).singleOrNull(); + } + @Override public GraphQueryMethod getQueryMethod() { return queryMethod; @@ -113,12 +121,20 @@ abstract class GraphRepositoryQuery implements RepositoryQuery, ParameterResolve @SuppressWarnings({"unchecked", "rawtypes"}) - protected Object createPage(Iterable result, Pageable pageable) { + protected Object createPage(Iterable result, Pageable pageable, Long count) { final List resultList = IteratorUtil.addToCollection(result, new ArrayList()); if (pageable==null) { return new PageImpl(resultList); } - final int currentTotal = pageable.getOffset() + pageable.getPageSize(); + long currentTotal; + if (count!=null) { + currentTotal = count; + } else { + int pageSize = pageable.getPageSize(); + long requestedCountStart = pageable.getOffset() * pageSize; + long resultSize = resultList.size(); + currentTotal = resultSize == pageSize ? requestedCountStart + pageSize : requestedCountStart+resultSize; + } return new PageImpl(resultList, pageable, currentTotal); } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/CypherDslRepositoryTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/CypherDslRepositoryTest.java index 2bf1a67d9..6b3e7395e 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/CypherDslRepositoryTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/CypherDslRepositoryTest.java @@ -19,6 +19,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import static org.hamcrest.CoreMatchers.is; import static org.neo4j.cypherdsl.CypherQuery.*; import static org.neo4j.cypherdsl.querydsl.CypherQueryDSL.*; import org.neo4j.cypherdsl.grammar.Execute; @@ -60,6 +61,7 @@ public class CypherDslRepositoryTest { private TestTeam team; private Map peopleParams; private Execute query = start(nodeByParameter("n", "people")).returns(identifier("n")); + private Execute countQuery = start(nodeByParameter("n", "people")).returns(count()); private Execute query2 = CypherQueryDSL.start(CypherQueryDSL.nodeByparameter(identifier(QPerson.person), "people")). where(toBooleanExpression(QPerson.person.name.eq("Michael"))). returns(identifier(QPerson.person)); @@ -76,6 +78,13 @@ public class CypherDslRepositoryTest { assertThat(result.getContent(), hasItems(team.michael,team.david)); } + @Test + public void testQueryPagedWithCount() throws Exception { + final Page result = personRepository.query(query, countQuery, peopleParams, new PageRequest(0, 1)); + assertThat(result.getContent(), hasItems(team.michael)); + assertThat(result.getTotalElements(), is(3L)); + } + @Test public void testQuery() throws Exception { final List result = personRepository.query(query, peopleParams).as(List.class); diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/GraphRepositoryTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/GraphRepositoryTest.java index 983bf618b..ea3e3bd1d 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/GraphRepositoryTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/GraphRepositoryTest.java @@ -177,12 +177,30 @@ public class GraphRepositoryTest { Person boss = personRepository.findBoss( testTeam.emil ); assertThat(boss, is( (Person)null )); } + + @Test @Transactional public void testCypherQueryWithNoResultsReturnsNullForPage() { Page people = personRepository.findSubordinates(testTeam.michael, new PageRequest(0, 10)); assertEquals(true, people.getContent().isEmpty()); } + @Test @Transactional + public void testCypherQueryForPage() { + Page people = personRepository.findSubordinates(testTeam.emil, new PageRequest(0, 10)); + assertEquals(2, people.getContent().size()); + assertEquals(2, people.getTotalElements()); + assertEquals(1, people.getTotalPages()); + } + + @Test @Transactional + public void testCypherQueryForPageWithCount() { + Page people = personRepository.findSubordinatesWithCount(testTeam.emil, new PageRequest(0, 1)); + assertEquals(1, people.getContent().size()); + assertEquals(2, people.getTotalElements()); + assertEquals(2, people.getTotalPages()); + } + @Test @Transactional public void testFindPersonWithQueryAnnotationUsingLongAsParameter() { Person boss = personRepository.findBoss(testTeam.michael.getId()); @@ -204,12 +222,11 @@ public class GraphRepositoryTest { } @Test @Transactional - @Ignore("untyil cypher supports parameters in path's and skip, limit") public void testFindWithMultipleParameters() { final int depth = 1; final int limit = 2; Iterable teamMembers = personRepository.findSomeTeamMembers(testTeam.sdg.getName(), 0, limit, depth); - assertThat(asCollection(teamMembers), hasItems(testTeam.michael, testTeam.david)); + assertThat(asCollection(teamMembers), hasItems(testTeam.david, testTeam.emil)); } @Test @Transactional @@ -403,7 +420,6 @@ public class GraphRepositoryTest { GraphDatabaseService gdb; @Test - @Ignore public void testFindMultiThreaded() throws Exception { final Car car = new TransactionTemplate(transactionManager).execute(new TransactionCallback() { @Override diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/PersonRepository.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/PersonRepository.java index c47eacdd7..95d0e05fb 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/PersonRepository.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/PersonRepository.java @@ -54,7 +54,7 @@ public interface PersonRepository extends GraphRepository, NamedIndexRep @Query("start member=node({p_person}) match team-[:persons]->member<-[?:boss]-boss return member") Iterable nonWorkingQuery(@Param("p_person") Person person); - @Query("start team=node:Group(name = {p_team}) match (team)-[:persons*1..1]->(member) return member.name,member.age skip {skip} limit {limit}") + @Query("start team=node:Group(name = {p_team}) match (team)-[:persons*1..1]->(member) return member order by member.name skip {`skip`} limit {`limit`}") Iterable findSomeTeamMembers(@Param("p_team") String team, @Param("skip") Integer skip,@Param("limit") Integer limit,@Param("depth") Integer depth); @Query("start person=node({p_person}) match (boss)-[:boss]->(person) return boss") @@ -66,6 +66,9 @@ public interface PersonRepository extends GraphRepository, NamedIndexRep @Query("start boss=node({0}) match (boss)-[:boss]->(person) return person order by count(*)") Page findSubordinates(Person boss,Pageable page); + @Query(value = "start boss=node({0}) match (boss)-[:boss]->(person) return person order by count(*)",countQuery = "start boss=node({0}) match (boss)-[:boss]->(person) with person return count(*)") + Page findSubordinatesWithCount(Person boss,Pageable page); + Group findTeam(@Param("p_person") Person person); @Query("start team=node({p_team}) match (team)-[:persons]->(member) return member") diff --git a/src/docbkx/reference/programming-model/repositories.xml b/src/docbkx/reference/programming-model/repositories.xml index b79635f98..9b0377b4f 100644 --- a/src/docbkx/reference/programming-model/repositories.xml +++ b/src/docbkx/reference/programming-model/repositories.xml @@ -131,6 +131,10 @@ For using the named parameters you have to either annotate the parameters of the method with the @Param("node") annotation or enable debug symbols. Indexed parameters are always usable. + + + If it is required that paged results return the correct total count, the @Query annotation can be supplied with a count query in the countQuery + attribute. This query is executed separately after the result query and its result is used to populate the totalCount property of the returned Page. Gremlin queries can be used similarly, the @Query annotation would just need a type=QueryType.GREMLIN attribute. @@ -143,7 +147,8 @@ Spring Data Neo4j also supports the notion of named queries which are externalized in property-config-files (META-INF/neo4j-named-queries.properties). Those files have the format: Entity.finderName=query (e.g. Person.findBoss=start p=node({0}) match (p)<-[:BOSS]-(boss) return boss). - Otherwise named queries support the same parameters as annotated queries. + Otherwise named queries support the same parameters as annotated queries. For count queries the lookup name is Entity.finderName.count=count-query. + The default query lookup names can be overriden by using an @Query annotation with queryName="my-query-name" or countQueryName="my-query-name".
@@ -266,7 +271,8 @@ Iterable findByParentAgeAndMarried(int age, boolean married) Cypher-DSL repository Spring Data Neo4j supports the new Cypher-DSL to write Cypher queries in a statically typed way. Just by including - CypherDslRepository to your repository you get the Page<T> query(Execute query, params, Pageable page) + CypherDslRepository to your repository you get the Page<T> query(Execute query, params, Pageable page), + Page<T> query(Execute query, Execute countQuery, params, Pageable page) and the EndResult<T> query(Execute query, params);. The result type of the Cypher-DSL builder is called Execute.