Skip to content

Commit cfb2a87

Browse files
GH-2139 - Fix classcast exception for paginated projections.
We used the ResultProcessor wrong. It must be applied on the whole page or slice, and not on the intermediate result (collection). This closes #2139.
1 parent 027e422 commit cfb2a87

File tree

3 files changed

+62
-25
lines changed

3 files changed

+62
-25
lines changed

src/main/java/org/springframework/data/neo4j/repository/query/AbstractNeo4jQuery.java

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -94,18 +94,15 @@ public final Object execute(Object[] parameters) {
9494
(EntityInstanceWithSource) OptionalUnwrappingConverter.INSTANCE.convert(source));
9595
}
9696

97-
Object processedResult = resultProcessor.processResult(rawResult, preparingConverter);
98-
9997
if (queryMethod.isPageQuery()) {
100-
return createPage(parameterAccessor, (List<?>) processedResult);
98+
rawResult = createPage(parameterAccessor, (List<?>) rawResult);
10199
} else if (queryMethod.isSliceQuery()) {
102-
return createSlice(incrementLimit, parameterAccessor, (List<?>) processedResult);
103-
} else {
104-
return processedResult;
100+
rawResult = createSlice(incrementLimit, parameterAccessor, (List<?>) rawResult);
105101
}
102+
return resultProcessor.processResult(rawResult, preparingConverter);
106103
}
107104

108-
private Page<?> createPage(Neo4jParameterAccessor parameterAccessor, List<?> processedResult) {
105+
private Page<?> createPage(Neo4jParameterAccessor parameterAccessor, List<?> rawResult) {
109106

110107
LongSupplier totalSupplier = () -> {
111108

@@ -115,26 +112,26 @@ private Page<?> createPage(Neo4jParameterAccessor parameterAccessor, List<?> pro
115112

116113
return neo4jOperations.toExecutableQuery(countQuery).getRequiredSingleResult();
117114
};
118-
return PageableExecutionUtils.getPage(processedResult, parameterAccessor.getPageable(), totalSupplier);
115+
return PageableExecutionUtils.getPage(rawResult, parameterAccessor.getPageable(), totalSupplier);
119116
}
120117

121-
private Slice<?> createSlice(boolean incrementLimit, Neo4jParameterAccessor parameterAccessor, List<?> processedResults) {
118+
private Slice<?> createSlice(boolean incrementLimit, Neo4jParameterAccessor parameterAccessor, List<?> rawResult) {
122119

123120
Pageable pageable = parameterAccessor.getPageable();
124121

125122
if (incrementLimit) {
126-
return new SliceImpl<>(
127-
processedResults.subList(0, Math.min(processedResults.size(), pageable.getPageSize())),
123+
return new SliceImpl<>(
124+
rawResult.subList(0, Math.min(rawResult.size(), pageable.getPageSize())),
128125
PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), pageable.getSort()),
129-
processedResults.size() > pageable.getPageSize()
126+
rawResult.size() > pageable.getPageSize()
130127
);
131128
} else {
132129
PreparedQuery<Long> countQuery = getCountQuery(parameterAccessor)
133130
.orElseGet(() -> prepareQuery(Long.class, Collections.emptyList(), parameterAccessor,
134131
Neo4jQueryType.COUNT, null, UnaryOperator.identity()));
135132
long total = neo4jOperations.toExecutableQuery(countQuery).getRequiredSingleResult();
136133
return new SliceImpl<>(
137-
processedResults,
134+
rawResult,
138135
pageable,
139136
pageable.getOffset() + pageable.getPageSize() < total
140137
);

src/test/java/org/springframework/data/neo4j/integration/imperative/ProjectionIT.java

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,24 @@
1717

1818
import static org.assertj.core.api.Assertions.assertThat;
1919

20+
import java.util.AbstractMap;
2021
import java.util.Collection;
22+
import java.util.Map;
2123

2224
import org.junit.jupiter.api.BeforeEach;
2325
import org.junit.jupiter.api.Test;
2426
import org.neo4j.driver.Driver;
2527
import org.neo4j.driver.Session;
2628
import org.neo4j.driver.Transaction;
29+
import org.neo4j.driver.Values;
2730
import org.springframework.beans.factory.annotation.Autowired;
2831
import org.springframework.context.annotation.Bean;
2932
import org.springframework.context.annotation.Configuration;
33+
import org.springframework.data.domain.Page;
34+
import org.springframework.data.domain.PageRequest;
35+
import org.springframework.data.domain.Pageable;
36+
import org.springframework.data.domain.Slice;
37+
import org.springframework.data.domain.Sort;
3038
import org.springframework.data.neo4j.config.AbstractNeo4jConfig;
3139
import org.springframework.data.neo4j.integration.shared.common.NamesOnly;
3240
import org.springframework.data.neo4j.integration.shared.common.NamesOnlyDto;
@@ -40,11 +48,13 @@
4048

4149
/**
4250
* @author Gerrit Meier
51+
* @author Michael J. Simons
4352
*/
4453
@Neo4jIntegrationTest
4554
class ProjectionIT {
4655

4756
private static final String FIRST_NAME = "Hans";
57+
private static final String FIRST_NAME2 = "Lieschen";
4858
private static final String LAST_NAME = "Mueller";
4959
private static final String CITY = "Braunschweig";
5060

@@ -64,8 +74,15 @@ void setup() {
6474

6575
transaction.run("MATCH (n) detach delete n");
6676

67-
transaction.run("CREATE (:Person{firstName:'" + FIRST_NAME + "', lastName:'" + LAST_NAME + "'})" + "-[:LIVES_AT]->"
68-
+ "(:Address{city:'" + CITY + "'})");
77+
for (Map.Entry<String, String> person : new Map.Entry[] {
78+
new AbstractMap.SimpleEntry(FIRST_NAME, LAST_NAME),
79+
new AbstractMap.SimpleEntry(FIRST_NAME2, LAST_NAME),
80+
}) {
81+
transaction.run(" MERGE (address:Address{city: $city})"
82+
+ "CREATE (:Person{firstName: $firstName, lastName: $lastName})"
83+
+ "-[:LIVES_AT]-> (address)",
84+
Values.parameters("firstName", person.getKey(), "lastName", person.getValue(), "city", CITY));
85+
}
6986

7087
transaction.commit();
7188
transaction.close();
@@ -74,16 +91,14 @@ void setup() {
7491

7592
@Test
7693
void loadNamesOnlyProjection(@Autowired ProjectionPersonRepository repository) {
77-
Collection<NamesOnly> people = repository.findByLastName(LAST_NAME);
78-
assertThat(people).hasSize(1);
7994

80-
NamesOnly person = people.iterator().next();
81-
assertThat(person.getFirstName()).isEqualTo(FIRST_NAME);
82-
assertThat(person.getLastName()).isEqualTo(LAST_NAME);
95+
Collection<NamesOnly> people = repository.findByLastName(LAST_NAME);
96+
assertThat(people).hasSize(2);
8397

84-
String expectedFullName = FIRST_NAME + " " + LAST_NAME;
85-
assertThat(person.getFullName()).isEqualTo(expectedFullName);
98+
assertThat(people).extracting(NamesOnly::getFirstName).containsExactlyInAnyOrder(FIRST_NAME, FIRST_NAME2);
99+
assertThat(people).extracting(NamesOnly::getLastName).containsOnly(LAST_NAME);
86100

101+
assertThat(people).extracting(NamesOnly::getFullName).containsExactlyInAnyOrder(FIRST_NAME + " " + LAST_NAME, FIRST_NAME2 + " " + LAST_NAME);
87102
}
88103

89104
@Test
@@ -153,10 +168,34 @@ void findDynamicProjectionForNamesOnlyDto(@Autowired ProjectionPersonRepository
153168

154169
}
155170

171+
@Test // GH-2139
172+
void projectionsShouldBePaginatable(@Autowired ProjectionPersonRepository repository) {
173+
174+
Page<NamesOnly> people = repository.findAllProjectedBy(PageRequest.of(1, 1, Sort.by("firstName").descending()));
175+
assertThat(people.hasPrevious()).isTrue();
176+
assertThat(people.hasNext()).isFalse();
177+
assertThat(people).hasSize(1);
178+
assertThat(people).extracting(NamesOnly::getFullName).containsExactly(FIRST_NAME + " " + LAST_NAME);
179+
}
180+
181+
@Test // GH-2139
182+
void projectionsShouldBeSliceable(@Autowired ProjectionPersonRepository repository) {
183+
184+
Slice<NamesOnly> people = repository.findSliceProjectedBy(PageRequest.of(1, 1, Sort.by("firstName").descending()));
185+
assertThat(people.hasPrevious()).isTrue();
186+
assertThat(people.hasNext()).isFalse();
187+
assertThat(people).hasSize(1);
188+
assertThat(people).extracting(NamesOnly::getFullName).containsExactly(FIRST_NAME + " " + LAST_NAME);
189+
}
190+
156191
interface ProjectionPersonRepository extends Neo4jRepository<Person, Long> {
157192

158193
Collection<NamesOnly> findByLastName(String lastName);
159194

195+
Page<NamesOnly> findAllProjectedBy(Pageable pageable);
196+
197+
Slice<NamesOnly> findSliceProjectedBy(Pageable pageable);
198+
160199
Collection<PersonSummary> findByFirstName(String firstName);
161200

162201
Collection<NamesOnlyDto> findByFirstNameAndLastName(String firstName, String lastName);
@@ -175,5 +214,4 @@ public Driver driver() {
175214
}
176215

177216
}
178-
179217
}

src/test/kotlin/org/springframework/data/neo4j/integration/shared/common/KotlinPerson.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ import org.springframework.data.neo4j.core.schema.TargetNode
2828
* @author Michael J. Simons
2929
*/
3030
@Node
31-
data class KotlinPerson(@Id @GeneratedValue val id: Long, val name: String,
32-
@Relationship("WORKS_IN") val clubs:List<KotlinClubRelationship>)
31+
data class KotlinPerson(@Id @GeneratedValue val id: Long?, val name: String,
32+
@Relationship("WORKS_IN") val clubs: List<KotlinClubRelationship>) {
33+
constructor(name: String, clubs: List<KotlinClubRelationship>) : this(null, name, clubs)
34+
}
3335

3436
@RelationshipProperties
3537
data class KotlinClubRelationship(@Id @GeneratedValue val id: Long, val since: Int, @TargetNode val club: KotlinClub)

0 commit comments

Comments
 (0)