DATAGRAPH-323 - Support for count-queries for page results
This commit is contained in:
@@ -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 "";
|
||||
}
|
||||
|
||||
@@ -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<S extends PropertyContainer, T> im
|
||||
return findAll(); // todo
|
||||
}
|
||||
|
||||
@Override
|
||||
public EndResult<T> query(String query, Map<String, Object> params) {
|
||||
return template.query(query, params).to(clazz);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<T> findAll(final Pageable pageable) {
|
||||
int count = pageable.getPageSize();
|
||||
@@ -442,9 +448,21 @@ public abstract class AbstractGraphRepository<S extends PropertyContainer, T> im
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Page<T> query(Execute query, Map<String, Object> params, Pageable page) {
|
||||
public Page<T> query(Execute query, Execute countQuery, Map<String, Object> 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<Object> 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<T>(result.getContent(),page, count);
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Page<T> query(Execute query, Map<String, Object> params, Pageable page) {
|
||||
return query(query, null, params, page);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
|
||||
@@ -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<T> extends PagingAndSortingRepository<T, Long> {
|
||||
|
||||
Class getStoredJavaType(Object entity);
|
||||
|
||||
EndResult<T> query(String query, Map<String, Object> params);
|
||||
|
||||
}
|
||||
@@ -28,5 +28,6 @@ import java.util.Map;
|
||||
*/
|
||||
public interface CypherDslRepository<T> {
|
||||
Page<T> query(Execute query, Map<String, Object> params, Pageable page);
|
||||
Page<T> query(Execute query, Execute countQuery, Map<String, Object> params, Pageable page);
|
||||
EndResult<T> query(Execute query, Map<String, Object> params);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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<String, Object> 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<String,Object> 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<Person> 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<Person> result = personRepository.query(query, peopleParams).as(List.class);
|
||||
|
||||
@@ -177,12 +177,30 @@ public class GraphRepositoryTest {
|
||||
Person boss = personRepository.findBoss( testTeam.emil );
|
||||
assertThat(boss, is( (Person)null ));
|
||||
}
|
||||
|
||||
|
||||
@Test @Transactional
|
||||
public void testCypherQueryWithNoResultsReturnsNullForPage() {
|
||||
Page<Person> people = personRepository.findSubordinates(testTeam.michael, new PageRequest(0, 10));
|
||||
assertEquals(true, people.getContent().isEmpty());
|
||||
}
|
||||
|
||||
@Test @Transactional
|
||||
public void testCypherQueryForPage() {
|
||||
Page<Person> 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<Person> 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<Person> 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<Car>() {
|
||||
@Override
|
||||
|
||||
@@ -54,7 +54,7 @@ public interface PersonRepository extends GraphRepository<Person>, NamedIndexRep
|
||||
@Query("start member=node({p_person}) match team-[:persons]->member<-[?:boss]-boss return member")
|
||||
Iterable<MemberData> 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<Person> 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<Person>, NamedIndexRep
|
||||
@Query("start boss=node({0}) match (boss)-[:boss]->(person) return person order by count(*)")
|
||||
Page<Person> 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<Person> findSubordinatesWithCount(Person boss,Pageable page);
|
||||
|
||||
Group findTeam(@Param("p_person") Person person);
|
||||
|
||||
@Query("start team=node({p_team}) match (team)-[:persons]->(member) return member")
|
||||
|
||||
@@ -131,6 +131,10 @@
|
||||
|
||||
For using the named parameters you have to either annotate the parameters of the method with the
|
||||
<code>@Param("node")</code> annotation or enable debug symbols. Indexed parameters are always usable.
|
||||
</para>
|
||||
<para>
|
||||
If it is required that paged results return the correct total count, the <code>@Query</code> annotation can be supplied with a count query in the <code>countQuery</code>
|
||||
attribute. This query is executed separately after the result query and its result is used to populate the <code>totalCount</code> property of the returned Page.
|
||||
</para>
|
||||
<para>
|
||||
Gremlin queries can be used similarly, the <code>@Query</code> annotation would just need a <code>type=QueryType.GREMLIN</code> attribute.
|
||||
@@ -143,7 +147,8 @@
|
||||
<para>Spring Data Neo4j also supports the notion of named queries which are externalized in property-config-files
|
||||
(<code>META-INF/neo4j-named-queries.properties</code>). Those files have the format:
|
||||
<code>Entity.finderName=query</code> (e.g. <code>Person.findBoss=start p=node({0}) match (p)<-[:BOSS]-(boss) return boss</code>).
|
||||
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 <code>Entity.finderName.count=count-query</code>.
|
||||
The default query lookup names can be overriden by using an <code>@Query</code> annotation with <code>queryName="my-query-name"</code> or <code>countQueryName="my-query-name"</code>.
|
||||
</para>
|
||||
</section>
|
||||
<section>
|
||||
@@ -266,7 +271,8 @@ Iterable<Person> findByParentAgeAndMarried(int age, boolean married)
|
||||
<title>Cypher-DSL repository</title>
|
||||
<para>
|
||||
Spring Data Neo4j supports the new Cypher-DSL to write Cypher queries in a statically typed way. Just by including
|
||||
<code>CypherDslRepository</code> to your repository you get the <code>Page<T> query(Execute query, params, Pageable page)</code>
|
||||
<code>CypherDslRepository</code> to your repository you get the <code>Page<T> query(Execute query, params, Pageable page)</code>,
|
||||
<code>Page<T> query(Execute query, Execute countQuery, params, Pageable page)</code>
|
||||
and the <code>EndResult<T> query(Execute query, params);</code>. The result type of the Cypher-DSL builder is called
|
||||
<code>Execute</code>.
|
||||
</para>
|
||||
|
||||
Reference in New Issue
Block a user