DATAGRAPH-440 Add support for Slice API

This commit is contained in:
Michael Hunger
2014-03-30 14:37:13 +02:00
parent f116325fbf
commit 3e0e0e70e6
15 changed files with 198 additions and 46 deletions

View File

@@ -17,6 +17,10 @@ package org.springframework.data.neo4j.rest;
import org.neo4j.helpers.collection.IteratorUtil;
import org.neo4j.rest.graphdb.util.ConvertedResult;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.neo4j.conversion.ContainerConverter;
import org.springframework.data.neo4j.conversion.EndResult;
@@ -54,6 +58,16 @@ class SpringEndResult<R> implements EndResult<R> {
return ContainerConverter.toContainer(container,this);
}
@Override
public Slice<R> slice(int page, int pageSize) {
return slice(new PageRequest(page,pageSize));
}
@Override
public Slice<R> slice(Pageable page) {
return ContainerConverter.slice(this, page);
}
@Override
public void finish()
{

View File

@@ -17,6 +17,10 @@ package org.springframework.data.neo4j.rest;
import org.neo4j.rest.graphdb.util.ConvertedResult;
import org.neo4j.rest.graphdb.util.ResultConverter;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.neo4j.conversion.ContainerConverter;
import org.springframework.data.neo4j.conversion.EndResult;
import org.springframework.data.neo4j.conversion.Result;
import org.springframework.data.neo4j.mapping.MappingPolicy;
@@ -53,6 +57,16 @@ class SpringRestResult<T> implements Result<T> {
queryResult.handle(new SpringHandler<T>(handler));
}
@Override
public Slice<T> slice(int page, int pageSize) {
return slice(new PageRequest(page,pageSize));
}
@Override
public Slice<T> slice(Pageable page) {
return ContainerConverter.slice( queryResult, page );
}
@Override
public Iterator<T> iterator() {
return queryResult.iterator();

View File

@@ -16,8 +16,7 @@
package org.springframework.data.neo4j.conversion;
import org.neo4j.helpers.collection.IteratorUtil;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.*;
import java.util.*;
@@ -35,6 +34,19 @@ public class ContainerConverter {
if (Page.class.isAssignableFrom(container)) {
return (C) new PageImpl<T>(list);
}
if (Slice.class.isAssignableFrom(container)) {
return (C) new SliceImpl<T>(list);
}
return (C) list;
}
public static <T> Slice<T> slice(Iterable<T> data, Pageable page) {
int offset = page.getOffset();
Iterator<T> it = data.iterator();
while (it.hasNext() && offset > 0) { it.next(); offset--; }
int pageSize = page.getPageSize();
List<T> result = new ArrayList<>(pageSize);
while (it.hasNext() && pageSize > 0) { result.add(it.next()); pageSize--; }
return new SliceImpl<T>(result,page,it.hasNext());
}
}

View File

@@ -16,6 +16,9 @@
package org.springframework.data.neo4j.conversion;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
/**
* @author mh
* @since 28.06.11
@@ -25,5 +28,7 @@ public interface EndResult<R> extends Iterable<R> {
R singleOrNull();
void handle(Handler<R> handler);
<C extends Iterable<R>> C as(Class<C> container);
Slice<R> slice(int page, int pageSize);
Slice<R> slice(Pageable page);
void finish();
}

View File

@@ -20,6 +20,9 @@ import org.neo4j.graphdb.index.IndexHits;
import org.neo4j.helpers.collection.ClosableIterable;
import org.neo4j.helpers.collection.IteratorUtil;
import org.neo4j.helpers.collection.IteratorWrapper;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.neo4j.mapping.MappingPolicy;
import java.util.Arrays;
@@ -128,6 +131,16 @@ public class QueryResultBuilder<T> implements Result<T> {
return ContainerConverter.toContainer(container, this);
}
@Override
public Slice<R> slice(Pageable page) {
return ContainerConverter.slice(this,page);
}
@Override
public Slice<R> slice(int page, int size) {
return ContainerConverter.slice(this,new PageRequest(page,size));
}
@Override
public void finish()
{
@@ -142,6 +155,16 @@ public class QueryResultBuilder<T> implements Result<T> {
return ContainerConverter.toContainer(container, this);
}
@Override
public Slice<T> slice(Pageable page) {
return ContainerConverter.slice(this,page);
}
@Override
public Slice<T> slice(int page, int size) {
return slice(new PageRequest(page,size));
}
@Override
public void handle(Handler<T> handler) {
try {

View File

@@ -23,10 +23,7 @@ import org.neo4j.graphdb.index.IndexHits;
import org.neo4j.graphdb.index.ReadableIndex;
import org.neo4j.helpers.collection.ClosableIterable;
import org.springframework.dao.DataRetrievalFailureException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.*;
import org.springframework.data.neo4j.conversion.EndResult;
import org.springframework.data.neo4j.mapping.Neo4jPersistentEntity;
import org.springframework.data.neo4j.mapping.Neo4jPersistentProperty;
@@ -375,6 +372,13 @@ public abstract class AbstractGraphRepository<S extends PropertyContainer, T> im
return new PageImpl<T>(result, pageable, total);
}
private SliceImpl<T> extractSlice(Pageable pageable, int count, int offset, Iterator<T> iterator) {
final List<T> result = new ArrayList<T>(count);
int total=subList(offset, count, iterator, result);
boolean hasNext = iterator.hasNext();
return new SliceImpl<>(result, pageable, hasNext);
}
private int subList(int skip, int limit, Iterator<T> source, final List<T> list) {
int count=0;
while (source.hasNext()) {

View File

@@ -45,7 +45,7 @@ class CypherGraphRepositoryQuery extends GraphRepositoryQuery {
if (pageable==null) {
return baseQuery;
}
return baseQuery + " skip "+pageable.getOffset() + " limit " + pageable.getPageSize();
return baseQuery + " skip "+pageable.getOffset() + " limit " + pageable.getPageSize() + 1 ;
}
private String addSorting(String baseQuery, Sort sort) {

View File

@@ -20,6 +20,7 @@ import org.neo4j.graphdb.Relationship;
import org.neo4j.helpers.collection.IteratorUtil;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.SliceImpl;
import org.springframework.data.neo4j.conversion.EndResult;
import org.springframework.data.neo4j.core.GraphDatabase;
import org.springframework.data.neo4j.support.Neo4jTemplate;
@@ -102,10 +103,10 @@ abstract class GraphRepositoryQuery implements RepositoryQuery, ParameterResolve
GraphQueryMethod queryMethod = getQueryMethod();
final QueryEngine<?> queryEngine = getQueryEngine();
final Class<?> compoundType = queryMethod.getCompoundType();
if (queryMethod.isPageQuery()) {
if (queryMethod.isPageQuery() || queryMethod.isSliceQuery()) {
@SuppressWarnings("unchecked") final Iterable<?> result = queryEngine.query(queryString, params).to(compoundType);
Long count = computeCount(params);
return createPage(result, accessor.getPageable(),count);
return createPage(result, accessor.getPageable(),count, queryMethod.isPageQuery());
}
if (queryMethod.isIterableResult()) {
final EndResult<?> result = queryEngine.query(queryString, params).to(compoundType);
@@ -129,21 +130,28 @@ abstract class GraphRepositoryQuery implements RepositoryQuery, ParameterResolve
@SuppressWarnings({"unchecked", "rawtypes"})
protected Object createPage(Iterable<?> result, Pageable pageable, Long count) {
protected Object createPage(Iterable<?> result, Pageable pageable, Long count, boolean isPageQuery) {
final List resultList = IteratorUtil.addToCollection(result, new ArrayList());
if (pageable==null) {
return new PageImpl(resultList);
return isPageQuery ? new PageImpl(resultList) : new SliceImpl(resultList);
}
long currentTotal;
int pageSize = pageable.getPageSize();
int requestedCountStart = pageable.getOffset();
int resultSize = resultList.size();
int currentTotal;
if (count!=null) {
currentTotal = count;
currentTotal = count.intValue();
} else {
int pageSize = pageable.getPageSize();
long requestedCountStart = pageable.getOffset() * pageSize;
long resultSize = resultList.size();
currentTotal = resultSize == pageSize ? requestedCountStart + pageSize : requestedCountStart+resultSize;
if (resultSize == pageSize) currentTotal = requestedCountStart + pageSize;
else currentTotal = requestedCountStart + resultSize;
}
return new PageImpl(resultList, pageable, currentTotal);
int resultWindowSize = Math.min(resultSize, pageSize);
boolean hasNext = resultWindowSize < resultSize;
List resultListPage = resultList.subList(0, resultWindowSize);
return isPageQuery ?
new PageImpl(resultListPage, pageable, currentTotal) :
new SliceImpl(resultListPage,pageable, hasNext);
}
protected abstract CypherQueryEngine getQueryEngine();

View File

@@ -0,0 +1,39 @@
package org.springframework.data.neo4j.conversion;
import org.junit.Test;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Slice;
import static java.util.Arrays.asList;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;
/**
* @author mh
* @since 30.03.14
*/
public class ContainerConverterTest {
@Test
public void testSliceFirst() throws Exception {
Slice<Integer> slice = ContainerConverter.slice(asList(1, 2, 3), new PageRequest(0, 1));
assertThat(slice.getContent(), hasItem(1));
assertThat(slice.getSize(), is(1));
assertThat(slice.hasNext(), is(true));
}
@Test
public void testSliceLast() throws Exception {
Slice<Integer> slice = ContainerConverter.slice(asList(1,2,3), new PageRequest(2, 1));
assertThat(slice.getContent(), hasItem(3));
assertThat(slice.getSize(), is(1));
assertThat(slice.hasNext(), is(false));
}
@Test
public void testSliceAll() throws Exception {
Slice<Integer> slice = ContainerConverter.slice(asList(1, 2, 3), new PageRequest(0, 3));
assertThat(slice.getContent(), hasItems(1, 2, 3));
assertThat(slice.getSize(), is(3));
assertThat(slice.hasNext(), is(false));
}
}

View File

@@ -17,16 +17,20 @@ package org.springframework.data.neo4j.conversion;
import org.junit.Before;
import org.junit.Test;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Slice;
import org.springframework.data.neo4j.annotation.MapResult;
import org.springframework.data.neo4j.support.conversion.EntityResultConverter;
import java.util.Collections;
import java.util.Map;
import java.util.*;
import static java.util.Arrays.asList;
import static org.hamcrest.CoreMatchers.hasItems;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.neo4j.helpers.collection.MapUtil.map;
import static org.springframework.data.neo4j.conversion.QueryResultBuilder.from;
/**
@@ -69,7 +73,19 @@ public class QueryResultBuilderTests {
@Test
public void testAs() throws Exception {
Map<String, Object> value1 = map("key", 1);
Map<String, Object> value2 = map("key", 2);
QueryResultBuilder<Map<String,Object>> builder = new QueryResultBuilder<Map<String,Object>>(Arrays.asList(value1, value2));
List<Map<String,Object>> list = builder.as(List.class);
assertEquals(2,list.size());
assertThat(list,hasItems(value1,value2));
Page<Map<String,Object>> page = builder.as(Page.class);
assertEquals(2,page.getNumberOfElements());
assertThat(page,hasItems(value1,value2));
Slice<Map<String,Object>> slice = builder.as(Slice.class);
assertEquals(2,slice.getNumberOfElements());
assertThat(page,hasItems(value1,value2));
}
@Test

View File

@@ -18,6 +18,7 @@ package org.springframework.data.neo4j.repositories;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort;
import org.springframework.data.neo4j.annotation.Query;
import org.springframework.data.neo4j.annotation.QueryResult;
@@ -78,6 +79,9 @@ public interface PersonRepository extends GraphRepository<Person>, NamedIndexRep
@Query("match (team)-[:persons]->(member) where id(team) = {p_team} return member")
Page<Person> findAllTeamMembersPaged(@Param("p_team") Group team, Pageable page);
@Query("match (team)-[:persons]->(member) where id(team) = {p_team} return member")
Slice<Person> findAllTeamMembersSliced(@Param("p_team") Group team, Pageable page);
@Query("match (team)-[:persons]->(member) where id(team) = {p_team} return member")
Iterable<Person> findAllTeamMembersSorted(@Param("p_team") Group team, Sort sort);

View File

@@ -28,6 +28,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort;
import org.springframework.data.neo4j.model.*;
import org.springframework.data.neo4j.repositories.*;
@@ -295,6 +296,30 @@ public class GraphRepositoryTests {
assertThat(teamMemberPage1, hasItem(testTeam.david));
}
@Test @Transactional
public void testFindSliced() {
final PageRequest page = new PageRequest(0, 1, Sort.Direction.ASC, "member.name","member.age");
Slice<Person> teamMemberSlice = personRepository.findAllTeamMembersSliced(testTeam.sdg, page);
assertThat(teamMemberSlice, hasItem(testTeam.david));
assertThat(teamMemberSlice.hasNext(),is(true));
}
@Test @Transactional
public void testFindSlicedLastPage() {
final PageRequest page = new PageRequest(2, 1, Sort.Direction.ASC, "member.name","member.age");
Slice<Person> teamMemberSlice = personRepository.findAllTeamMembersSliced(testTeam.sdg, page);
assertThat(teamMemberSlice, hasItem(testTeam.michael));
assertThat(teamMemberSlice.hasNext(),is(false));
}
@Test @Transactional
public void testFindSlicedLastAll() {
final PageRequest page = new PageRequest(0, 3, Sort.Direction.ASC, "member.name","member.age");
Slice<Person> teamMemberSlice = personRepository.findAllTeamMembersSliced(testTeam.sdg, page);
assertThat(teamMemberSlice, hasItems(testTeam.david, testTeam.emil, testTeam.michael));
assertThat(teamMemberSlice.hasNext(), is(false));
}
@Test @Transactional
public void testFindPagedDescending() {
final PageRequest page = new PageRequest(0, 2, Sort.Direction.DESC, "member.name");

View File

@@ -49,9 +49,8 @@
<varlistentry>
<term>Iterate over all nodes of a node entity type</term>
<listitem><para><code>EndResult&lt;T&gt; findAll()</code>
(supported in future versions:
<code>EndResult&lt;T&gt; findAll(Sort)</code> and
<code>Page&lt;T&gt; findAll(Pageable)</code>)</para></listitem>
<code>Page&lt;T&gt; findAll(Pageable)</code></para></listitem>
</varlistentry>
<varlistentry>
<term>Count the instances of the repository entity type</term>
@@ -125,7 +124,7 @@
will use the supplied query string. The named or indexed parameter <code>{node}</code> will be substituted by the actual method parameter.
Node and Relationship-Entities are handled directly, Iterables thereof as well. All other parameters are
replaced directly (i.e. Strings, Longs, etc). There is special support for the <code>Sort</code> and <code>Pageable</code>
parameters from Spring Data Commons, which are supported to add programmatic paging and sorting (alternatively
parameters from Spring Data Commons, which are supported to add programmatic paging, slicing and sorting (alternatively
static paging and sorting can be supplied in the query string itself).
For using the named parameters you have to either annotate the parameters of the method with the
@@ -133,7 +132,7 @@
</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.
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 <code>Page</code>.
</para>
</section>
@@ -148,7 +147,7 @@
</section>
<section>
<title>Query results</title>
<para>Typical results for queries are <code>Iterable&lt;Type&gt;, Iterable&lt;Map&lt;String,Object&gt;&gt;, Type and Page&lt;Type&gt;</code>.
<para>Typical results for queries are <code>Iterable&lt;Type&gt;, Iterable&lt;Map&lt;String,Object&gt;&gt;, Type, Slice&lt;Type&gt; and Page&lt;Type&gt;</code>.
Nodes and Relationships are converted to their respective Entities (if they exist). Other values are converted
using the registered Spring conversion services (e.g. enums).
</para>
@@ -532,4 +531,4 @@ Iterable<Person> friends = personRepository.findFriends(dave);
</para>
</note>
</section>
</section>
</section>

View File

@@ -85,28 +85,9 @@ public String singleMovieView(final Model model, @PathVariable String movieId) {
<programlisting language="java"><![CDATA[public interface MovieRepository ... {
Movie findById(String id);
Page<Movie> findByTitleLike(String title, Pageable page);
Slice<Movie> findAll(Pageable page);
}
]]></programlisting>
<!-- MOVE TO REFERENCE programlisting language="java"><![CDATA[public class CineastRepository {
Page<Movie> findByTitle(String title, PageRequest page);
....
public void List<Movie> findMovies(String query, int count) {
List<Movie> movies=new ArrayList<Movie>(count);
ClosableIterable<Movie> searchResults = movieRepository.findAllByQuery("title", query);
for (Movie movie : searchResults) {
movies.add(movie);
if (count- - == 0) break;
}
searchResults.close();
return movies;
}
}
The corresponding
repository method is called <code>findAllByQuery</code>. To restrict the size of the returned set we simply
added a limit that truncates the result.
]]></programlisting-->
</example>
</para>
</section>

View File

@@ -1,6 +1,14 @@
Spring Data Neo4j Changelog
===========================
Changes in version 3.1.0.M1 (2014-03-31)
----------------------------------------
* DATAGRAPH-440 Add support for Slice API
* DATAGRAPH-451 RelationshipEntities will be validated when manipulated
* DATAGRAPH-450 Updated example code
* DATAGRAPH-449 Updated docs for 3.x
* DATAGRAPH-452 - Adapted to changes in BeanWrapper generics.
Changes in version 3.0.1.RELEASE (2014-03-13)
---------------------------------------------
* DATAGRAPH-433 - Unique Creation of label based entities