Skip to content

Commit 70a4c6e

Browse files
committed
Exclude id if provided sort properties is keyset qualified
See GH-2996 Signed-off-by: Yanming Zhou <zhouyanming@gmail.com>
1 parent 5dd6fe5 commit 70a4c6e

File tree

5 files changed

+108
-16
lines changed

5 files changed

+108
-16
lines changed

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

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
* Delegate for keyset scrolling.
3535
*
3636
* @author Mark Paluch
37+
* @author Yanming Zhou
3738
* @since 3.1
3839
*/
3940
public class KeysetScrollDelegate {
@@ -142,19 +143,24 @@ public Sort createSort(Sort sort, JpaEntityInformation<?, ?> entity) {
142143

143144
Collection<String> sortById;
144145
Sort sortToUse;
145-
if (entity.hasCompositeId()) {
146-
sortById = new ArrayList<>(entity.getIdAttributeNames());
147-
} else {
148-
sortById = new ArrayList<>(1);
149-
sortById.add(entity.getRequiredIdAttribute().getName());
146+
if (entity.isKeysetQualified(sort.stream().map(Order::getProperty).toList())) {
147+
sortToUse = sort;
150148
}
149+
else {
150+
if (entity.hasCompositeId()) {
151+
sortById = new ArrayList<>(entity.getIdAttributeNames());
152+
} else {
153+
sortById = new ArrayList<>(1);
154+
sortById.add(entity.getRequiredIdAttribute().getName());
155+
}
151156

152-
sort.forEach(it -> sortById.remove(it.getProperty()));
157+
sort.forEach(it -> sortById.remove(it.getProperty()));
153158

154-
if (sortById.isEmpty()) {
155-
sortToUse = sort;
156-
} else {
157-
sortToUse = sort.and(Sort.by(sortById.toArray(new String[0])));
159+
if (sortById.isEmpty()) {
160+
sortToUse = sort;
161+
} else {
162+
sortToUse = sort.and(Sort.by(sortById.toArray(new String[0])));
163+
}
158164
}
159165

160166
return getSortOrders(sortToUse);

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
* @author Oliver Gierke
3232
* @author Thomas Darimont
3333
* @author Mark Paluch
34+
* @author Yanming Zhou
3435
*/
3536
public interface JpaEntityInformation<T, ID> extends EntityInformation<T, ID>, JpaEntityMetadata<T> {
3637

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

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

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

Lines changed: 56 additions & 5 deletions
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;
@@ -46,6 +47,7 @@
4647
import org.springframework.data.jpa.util.JpaMetamodel;
4748
import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper;
4849
import org.springframework.util.Assert;
50+
import org.springframework.util.StringUtils;
4951

5052
/**
5153
* Implementation of {@link org.springframework.data.repository.core.EntityInformation} that uses JPA {@link Metamodel}
@@ -57,6 +59,7 @@
5759
* @author Mark Paluch
5860
* @author Jens Schauder
5961
* @author Greg Turnquist
62+
* @author Yanming Zhou
6063
*/
6164
public class JpaMetamodelEntityInformation<T, ID> extends JpaEntityInformationSupport<T, ID> {
6265

@@ -265,12 +268,14 @@ public Map<String, Object> getKeyset(Iterable<String> propertyPaths, T entity) {
265268

266269
Map<String, Object> keyset = new LinkedHashMap<>();
267270

268-
if (hasCompositeId()) {
269-
for (String idAttributeName : getIdAttributeNames()) {
270-
keyset.put(idAttributeName, getter.apply(idAttributeName));
271+
if(!isKeysetQualified(propertyPaths)) {
272+
if (hasCompositeId()) {
273+
for (String idAttributeName : getIdAttributeNames()) {
274+
keyset.put(idAttributeName, getter.apply(idAttributeName));
275+
}
276+
} else {
277+
keyset.put(getIdAttribute().getName(), getId(entity));
271278
}
272-
} else {
273-
keyset.put(getIdAttribute().getName(), getId(entity));
274279
}
275280

276281
for (String propertyPath : propertyPaths) {
@@ -280,6 +285,52 @@ public Map<String, Object> getKeyset(Iterable<String> propertyPaths, T entity) {
280285
return keyset;
281286
}
282287

288+
@Override
289+
public boolean isKeysetQualified(Iterable<String> propertyPaths) {
290+
291+
if (propertyPaths.iterator().hasNext()) {
292+
for (String property : propertyPaths) {
293+
if (isUnique(property)) {
294+
return true;
295+
}
296+
}
297+
}
298+
299+
return false;
300+
}
301+
302+
@Nullable
303+
private boolean isUnique(String property) {
304+
305+
Class<?> clazz = getJavaType();
306+
307+
while (clazz != Object.class) {
308+
309+
try {
310+
Column column = clazz.getDeclaredField(property).getAnnotation(Column.class);
311+
if (column != null) {
312+
return column.unique();
313+
}
314+
} catch (NoSuchFieldException ex) {
315+
// ignore
316+
}
317+
318+
try {
319+
String getter = "get" + StringUtils.capitalize(property);
320+
Column column = clazz.getDeclaredMethod(getter).getAnnotation(Column.class);
321+
if (column != null) {
322+
return column.unique();
323+
}
324+
} catch (NoSuchMethodException ex) {
325+
// ignore
326+
}
327+
328+
clazz = clazz.getSuperclass();
329+
}
330+
331+
return false;
332+
}
333+
283334
private Function<String, Object> getPropertyValueFunction(Object entity) {
284335

285336
if (entity instanceof Tuple t) {

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

Lines changed: 4 additions & 0 deletions
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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,22 @@
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;
3132
import org.springframework.test.context.ContextConfiguration;
3233
import org.springframework.test.context.junit.jupiter.SpringExtension;
3334
import org.springframework.transaction.annotation.Transactional;
3435

36+
import java.util.List;
37+
import java.util.Map;
38+
3539
/**
3640
* Unit tests for {@link KeysetScrollSpecification}.
3741
*
3842
* @author Mark Paluch
43+
* @author Yanming Zhou
3944
*/
4045
@ExtendWith(SpringExtension.class)
4146
@ContextConfiguration({ "classpath:infrastructure.xml" })
@@ -74,4 +79,17 @@ void shouldSkipExistingIdentifiersInSort() {
7479
assertThat(sort).extracting(Order::getProperty).containsExactly("id", "firstname");
7580
}
7681

82+
@Test // GH-3013
83+
void shouldSkipIdentifiersInSortIfUniquePropertyPresent() {
84+
85+
JpaMetamodelEntityInformation<Product, Long> info = new JpaMetamodelEntityInformation<>(Product.class, em.getMetamodel(),
86+
em.getEntityManagerFactory().getPersistenceUnitUtil());
87+
Map<String, Object> keyset = info.getKeyset(List.of("code"), new Product());
88+
89+
assertThat(keyset).containsOnlyKeys("code");
90+
91+
Sort sort = KeysetScrollSpecification.createSort(ScrollPosition.keyset(), Sort.by("code"), info);
92+
93+
assertThat(sort).extracting(Order::getProperty).containsExactly("code");
94+
}
7795
}

0 commit comments

Comments
 (0)