Skip to content

Commit 444403a

Browse files
committed
Exclude id if provided sort properties is keyset qualified
See GH-2996
1 parent 018c2ac commit 444403a

File tree

5 files changed

+102
-17
lines changed

5 files changed

+102
-17
lines changed

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollSpecification.java

+17-11
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
*
4141
* @author Mark Paluch
4242
* @author Christoph Strobl
43+
* @author Yanming Zhou
4344
* @since 3.1
4445
*/
4546
public record KeysetScrollSpecification<T> (KeysetScrollPosition position, Sort sort,
@@ -63,21 +64,26 @@ public static Sort createSort(KeysetScrollPosition position, Sort sort, JpaEntit
6364

6465
KeysetScrollDelegate delegate = KeysetScrollDelegate.of(position.getDirection());
6566

66-
Collection<String> sortById;
6767
Sort sortToUse;
68-
if (entity.hasCompositeId()) {
69-
sortById = new ArrayList<>(entity.getIdAttributeNames());
70-
} else {
71-
sortById = new ArrayList<>(1);
72-
sortById.add(entity.getRequiredIdAttribute().getName());
73-
}
74-
75-
sort.forEach(it -> sortById.remove(it.getProperty()));
7668

77-
if (sortById.isEmpty()) {
69+
if (entity.isKeysetQualified(sort.stream().map(Order::getProperty).toList())) {
7870
sortToUse = sort;
7971
} else {
80-
sortToUse = sort.and(Sort.by(sortById.toArray(new String[0])));
72+
Collection<String> sortById;
73+
if (entity.hasCompositeId()) {
74+
sortById = new ArrayList<>(entity.getIdAttributeNames());
75+
} else {
76+
sortById = new ArrayList<>(1);
77+
sortById.add(entity.getRequiredIdAttribute().getName());
78+
}
79+
80+
sort.forEach(it -> sortById.remove(it.getProperty()));
81+
82+
if (sortById.isEmpty()) {
83+
sortToUse = sort;
84+
} else {
85+
sortToUse = sort.and(Sort.by(sortById.toArray(new String[0])));
86+
}
8187
}
8288

8389
return delegate.getSortOrders(sortToUse);

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaEntityInformation.java

+14-1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
* @author Oliver Gierke
3131
* @author Thomas Darimont
3232
* @author Mark Paluch
33+
* @author Yanming Zhou
3334
*/
3435
public interface JpaEntityInformation<T, ID> extends EntityInformation<T, ID>, JpaEntityMetadata<T> {
3536

@@ -79,12 +80,24 @@ public interface JpaEntityInformation<T, ID> extends EntityInformation<T, ID>, J
7980
Object getCompositeIdAttributeValue(Object id, String idAttribute);
8081

8182
/**
82-
* Extract a keyset for {@code propertyPaths} and the primary key (including composite key components if applicable).
83+
* Extract a keyset for {@code propertyPaths}, and the primary key (including composite key components if applicable)
84+
* if {@code propertyPaths} is not qualified.
8385
*
8486
* @param propertyPaths the property paths that make up the keyset in combination with the composite key components.
8587
* @param entity the entity to extract values from
8688
* @return a map mapping String representations of the paths to values from the entity.
8789
* @since 3.1
8890
*/
8991
Map<String, Object> getKeyset(Iterable<String> propertyPaths, T entity);
92+
93+
/**
94+
* Determine whether propertyPaths is qualified for keyset.
95+
*
96+
* @param propertyPaths the property paths that make up the keyset in combination with the composite key components.
97+
* @return {@code propertyPaths} is qualified for keyset.
98+
* @since 3.2
99+
*/
100+
default boolean isKeysetQualified(Iterable<String> propertyPaths) {
101+
return false;
102+
}
90103
}

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaMetamodelEntityInformation.java

+56-5
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.springframework.data.jpa.repository.support;
1717

18+
import jakarta.persistence.Column;
1819
import jakarta.persistence.IdClass;
1920
import jakarta.persistence.PersistenceUnitUtil;
2021
import jakarta.persistence.Tuple;
@@ -44,6 +45,7 @@
4445
import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper;
4546
import org.springframework.lang.Nullable;
4647
import org.springframework.util.Assert;
48+
import org.springframework.util.StringUtils;
4749

4850
/**
4951
* Implementation of {@link org.springframework.data.repository.core.EntityInformation} that uses JPA {@link Metamodel}
@@ -55,6 +57,7 @@
5557
* @author Mark Paluch
5658
* @author Jens Schauder
5759
* @author Greg Turnquist
60+
* @author Yanming Zhou
5861
*/
5962
public class JpaMetamodelEntityInformation<T, ID> extends JpaEntityInformationSupport<T, ID> {
6063

@@ -236,12 +239,14 @@ public Map<String, Object> getKeyset(Iterable<String> propertyPaths, T entity) {
236239

237240
Map<String, Object> keyset = new LinkedHashMap<>();
238241

239-
if (hasCompositeId()) {
240-
for (String idAttributeName : getIdAttributeNames()) {
241-
keyset.put(idAttributeName, getter.apply(idAttributeName));
242+
if(!isKeysetQualified(propertyPaths)) {
243+
if (hasCompositeId()) {
244+
for (String idAttributeName : getIdAttributeNames()) {
245+
keyset.put(idAttributeName, getter.apply(idAttributeName));
246+
}
247+
} else {
248+
keyset.put(getIdAttribute().getName(), getId(entity));
242249
}
243-
} else {
244-
keyset.put(getIdAttribute().getName(), getId(entity));
245250
}
246251

247252
for (String propertyPath : propertyPaths) {
@@ -251,6 +256,52 @@ public Map<String, Object> getKeyset(Iterable<String> propertyPaths, T entity) {
251256
return keyset;
252257
}
253258

259+
@Override
260+
public boolean isKeysetQualified(Iterable<String> propertyPaths) {
261+
262+
if (propertyPaths.iterator().hasNext()) {
263+
for (String property : propertyPaths) {
264+
if (isUnique(property)) {
265+
return true;
266+
}
267+
}
268+
}
269+
270+
return false;
271+
}
272+
273+
@Nullable
274+
private boolean isUnique(String property) {
275+
276+
Class<?> clazz = getJavaType();
277+
278+
while (clazz != Object.class) {
279+
280+
try {
281+
Column column = clazz.getDeclaredField(property).getAnnotation(Column.class);
282+
if (column != null) {
283+
return column.unique();
284+
}
285+
} catch (NoSuchFieldException ex) {
286+
// ignore
287+
}
288+
289+
try {
290+
String getter = "get" + StringUtils.capitalize(property);
291+
Column column = clazz.getDeclaredMethod(getter).getAnnotation(Column.class);
292+
if (column != null) {
293+
return column.unique();
294+
}
295+
} catch (NoSuchMethodException ex) {
296+
// ignore
297+
}
298+
299+
clazz = clazz.getSuperclass();
300+
}
301+
302+
return false;
303+
}
304+
254305
private Function<String, Object> getPropertyValueFunction(Object entity) {
255306

256307
if (entity instanceof Tuple t) {

spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/Product.java

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.springframework.data.jpa.domain.sample;
22

3+
import jakarta.persistence.Column;
34
import jakarta.persistence.Entity;
45
import jakarta.persistence.GeneratedValue;
56
import jakarta.persistence.Id;
@@ -9,6 +10,9 @@ public class Product {
910

1011
@Id @GeneratedValue private Long id;
1112

13+
@Column(unique = true)
14+
private String code;
15+
1216
public Long getId() {
1317
return id;
1418
}

spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/KeysetScrollSpecificationUnitTests.java

+11
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.springframework.data.domain.ScrollPosition;
2626
import org.springframework.data.domain.Sort;
2727
import org.springframework.data.domain.Sort.Order;
28+
import org.springframework.data.jpa.domain.sample.Product;
2829
import org.springframework.data.jpa.domain.sample.SampleWithIdClass;
2930
import org.springframework.data.jpa.domain.sample.User;
3031
import org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformation;
@@ -36,6 +37,7 @@
3637
* Unit tests for {@link KeysetScrollSpecification}.
3738
*
3839
* @author Mark Paluch
40+
* @author Yanming Zhou
3941
*/
4042
@ExtendWith(SpringExtension.class)
4143
@ContextConfiguration({ "classpath:infrastructure.xml" })
@@ -74,4 +76,13 @@ void shouldSkipExistingIdentifiersInSort() {
7476
assertThat(sort).extracting(Order::getProperty).containsExactly("id", "firstname");
7577
}
7678

79+
@Test // GH-3013
80+
void shouldSkipIdentifiersInSortIfUniquePropertyPresent() {
81+
82+
Sort sort = KeysetScrollSpecification.createSort(ScrollPosition.keyset(), Sort.by("code"),
83+
new JpaMetamodelEntityInformation<>(Product.class, em.getMetamodel(),
84+
em.getEntityManagerFactory().getPersistenceUnitUtil()));
85+
86+
assertThat(sort).extracting(Order::getProperty).containsExactly("code");
87+
}
7788
}

0 commit comments

Comments
 (0)