DATAGRAPH-323 - Support for count-queries for page results

This commit is contained in:
Michael Hunger
2013-01-28 13:20:19 +01:00
parent 8f88945582
commit 679cb6f886
10 changed files with 118 additions and 14 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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)&lt;-[: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&lt;T> query(Execute query, params, Pageable page)</code>
<code>CypherDslRepository</code> to your repository you get the <code>Page&lt;T> query(Execute query, params, Pageable page)</code>,
<code>Page&lt;T> query(Execute query, Execute countQuery, params, Pageable page)</code>
and the <code>EndResult&lt;T> query(Execute query, params);</code>. The result type of the Cypher-DSL builder is called
<code>Execute</code>.
</para>