Browse Source
Closes #2878 Original pull request #2885 See https://github.com/spring-projects/spring-data-commons/issues/2151pull/2898/head
30 changed files with 1311 additions and 337 deletions
@ -0,0 +1,61 @@
@@ -0,0 +1,61 @@
|
||||
/* |
||||
* Copyright 2023 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.data.jpa.repository.query; |
||||
|
||||
import java.util.List; |
||||
|
||||
/** |
||||
* Utility methods to obtain sublists. |
||||
* |
||||
* @author Mark Paluch |
||||
*/ |
||||
class CollectionUtils { |
||||
|
||||
/** |
||||
* Return the first {@code count} items from the list. |
||||
* |
||||
* @param count the number of first elements to be included in the returned list. |
||||
* @param list must not be {@literal null} |
||||
* @return the returned sublist if the {@code list} is greater {@code count}. |
||||
* @param <T> |
||||
*/ |
||||
public static <T> List<T> getFirst(int count, List<T> list) { |
||||
|
||||
if (count > 0 && list.size() > count) { |
||||
return list.subList(0, count); |
||||
} |
||||
|
||||
return list; |
||||
} |
||||
|
||||
/** |
||||
* Return the last {@code count} items from the list. |
||||
* |
||||
* @param count the number of last elements to be included in the returned list. |
||||
* @param list must not be {@literal null} |
||||
* @return the returned sublist if the {@code list} is greater {@code count}. |
||||
* @param <T> |
||||
*/ |
||||
public static <T> List<T> getLast(int count, List<T> list) { |
||||
|
||||
if (count > 0 && list.size() > count) { |
||||
return list.subList(list.size() - (count), list.size()); |
||||
} |
||||
|
||||
return list; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,83 @@
@@ -0,0 +1,83 @@
|
||||
/* |
||||
* Copyright 2023 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.data.jpa.repository.query; |
||||
|
||||
import jakarta.persistence.criteria.CriteriaBuilder; |
||||
import jakarta.persistence.criteria.CriteriaQuery; |
||||
import jakarta.persistence.criteria.Predicate; |
||||
import jakarta.persistence.criteria.Root; |
||||
|
||||
import java.util.Collection; |
||||
import java.util.LinkedHashSet; |
||||
import java.util.Set; |
||||
|
||||
import org.springframework.data.domain.KeysetScrollPosition; |
||||
import org.springframework.data.domain.Sort; |
||||
import org.springframework.data.jpa.repository.support.JpaEntityInformation; |
||||
import org.springframework.data.repository.query.ReturnedType; |
||||
import org.springframework.data.repository.query.parser.PartTree; |
||||
import org.springframework.lang.Nullable; |
||||
|
||||
/** |
||||
* Extension to {@link JpaQueryCreator} to create queries considering {@link KeysetScrollPosition keyset scrolling}. |
||||
* |
||||
* @author Mark Paluch |
||||
* @since 3.1 |
||||
*/ |
||||
class JpaKeysetScrollQueryCreator extends JpaQueryCreator { |
||||
|
||||
private final JpaEntityInformation<?, ?> entityInformation; |
||||
private final KeysetScrollPosition scrollPosition; |
||||
|
||||
public JpaKeysetScrollQueryCreator(PartTree tree, ReturnedType type, CriteriaBuilder builder, |
||||
ParameterMetadataProvider provider, JpaEntityInformation<?, ?> entityInformation, |
||||
KeysetScrollPosition scrollPosition) { |
||||
super(tree, type, builder, provider); |
||||
this.entityInformation = entityInformation; |
||||
this.scrollPosition = scrollPosition; |
||||
} |
||||
|
||||
@Override |
||||
protected CriteriaQuery<?> complete(@Nullable Predicate predicate, Sort sort, CriteriaQuery<?> query, |
||||
CriteriaBuilder builder, Root<?> root) { |
||||
|
||||
KeysetScrollSpecification<Object> keysetSpec = new KeysetScrollSpecification<>(scrollPosition, sort, |
||||
entityInformation); |
||||
Predicate keysetPredicate = keysetSpec.createPredicate(root, builder); |
||||
|
||||
CriteriaQuery<?> queryToUse = super.complete(predicate, keysetSpec.sort(), query, builder, root); |
||||
|
||||
if (keysetPredicate != null) { |
||||
if (queryToUse.getRestriction() != null) { |
||||
return queryToUse.where(builder.and(queryToUse.getRestriction(), keysetPredicate)); |
||||
} |
||||
return queryToUse.where(keysetPredicate); |
||||
} |
||||
|
||||
return queryToUse; |
||||
} |
||||
|
||||
@Override |
||||
Collection<String> getRequiredSelection(Sort sort, ReturnedType returnedType) { |
||||
|
||||
Sort sortToUse = KeysetScrollSpecification.createSort(scrollPosition, sort, entityInformation); |
||||
|
||||
Set<String> selection = new LinkedHashSet<>(returnedType.getInputProperties()); |
||||
sortToUse.forEach(it -> selection.add(it.getProperty())); |
||||
|
||||
return selection; |
||||
} |
||||
} |
||||
@ -0,0 +1,197 @@
@@ -0,0 +1,197 @@
|
||||
/* |
||||
* Copyright 2023 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.data.jpa.repository.query; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.Collections; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
|
||||
import org.springframework.data.domain.KeysetScrollPosition; |
||||
import org.springframework.data.domain.KeysetScrollPosition.Direction; |
||||
import org.springframework.data.domain.Sort; |
||||
import org.springframework.data.domain.Sort.Order; |
||||
import org.springframework.lang.Nullable; |
||||
|
||||
/** |
||||
* Delegate for keyset scrolling. |
||||
* |
||||
* @author Mark Paluch |
||||
* @since 3.1 |
||||
*/ |
||||
public class KeysetScrollDelegate { |
||||
|
||||
private static final KeysetScrollDelegate forward = new KeysetScrollDelegate(); |
||||
private static final KeysetScrollDelegate reverse = new ReverseKeysetScrollDelegate(); |
||||
|
||||
/** |
||||
* Factory method to obtain the right {@link KeysetScrollDelegate}. |
||||
* |
||||
* @param direction |
||||
* @return |
||||
*/ |
||||
public static KeysetScrollDelegate of(Direction direction) { |
||||
return direction == Direction.Forward ? forward : reverse; |
||||
} |
||||
|
||||
@Nullable |
||||
public <E, P> P createPredicate(KeysetScrollPosition keyset, Sort sort, QueryStrategy<E, P> strategy) { |
||||
|
||||
Map<String, Object> keysetValues = keyset.getKeys(); |
||||
|
||||
// first query doesn't come with a keyset
|
||||
if (keysetValues.isEmpty()) { |
||||
return null; |
||||
} |
||||
|
||||
List<P> or = new ArrayList<>(); |
||||
int i = 0; |
||||
|
||||
// progressive query building to reconstruct a query matching sorting rules
|
||||
for (Order order : sort) { |
||||
|
||||
if (!keysetValues.containsKey(order.getProperty())) { |
||||
throw new IllegalStateException(String |
||||
.format("KeysetScrollPosition does not contain all keyset values. Missing key: %s", order.getProperty())); |
||||
} |
||||
|
||||
List<P> sortConstraint = new ArrayList<>(); |
||||
|
||||
int j = 0; |
||||
for (Order inner : sort) { |
||||
|
||||
E propertyExpression = strategy.createExpression(inner.getProperty()); |
||||
Object o = keysetValues.get(inner.getProperty()); |
||||
|
||||
if (j >= i) { // tail segment
|
||||
|
||||
sortConstraint.add(strategy.compare(inner, propertyExpression, o)); |
||||
break; |
||||
} |
||||
|
||||
sortConstraint.add(strategy.compare(propertyExpression, o)); |
||||
j++; |
||||
} |
||||
|
||||
if (!sortConstraint.isEmpty()) { |
||||
or.add(strategy.and(sortConstraint)); |
||||
} |
||||
|
||||
i++; |
||||
} |
||||
|
||||
if (or.isEmpty()) { |
||||
return null; |
||||
} |
||||
|
||||
return strategy.or(or); |
||||
} |
||||
|
||||
protected Sort getSortOrders(Sort sort) { |
||||
return sort; |
||||
} |
||||
|
||||
@SuppressWarnings("unchecked") |
||||
protected <T> List<T> postProcessResults(List<T> result) { |
||||
return result; |
||||
} |
||||
|
||||
protected <T> List<T> getResultWindow(List<T> list, int limit) { |
||||
return CollectionUtils.getFirst(limit, list); |
||||
} |
||||
|
||||
/** |
||||
* Reverse scrolling variant applying {@link Direction#Backward}. In reverse scrolling, we need to flip directions for |
||||
* the actual query so that we do not get everything from the top position and apply the limit but rather flip the |
||||
* sort direction, apply the limit and then reverse the result to restore the actual sort order. |
||||
*/ |
||||
private static class ReverseKeysetScrollDelegate extends KeysetScrollDelegate { |
||||
|
||||
protected Sort getSortOrders(Sort sort) { |
||||
|
||||
List<Order> orders = new ArrayList<>(); |
||||
for (Order order : sort) { |
||||
orders.add(new Order(order.isAscending() ? Sort.Direction.DESC : Sort.Direction.ASC, order.getProperty())); |
||||
} |
||||
|
||||
return Sort.by(orders); |
||||
} |
||||
|
||||
@Override |
||||
protected <T> List<T> postProcessResults(List<T> result) { |
||||
Collections.reverse(result); |
||||
return result; |
||||
} |
||||
|
||||
@Override |
||||
protected <T> List<T> getResultWindow(List<T> list, int limit) { |
||||
return CollectionUtils.getLast(limit, list); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Adapter to construct scroll queries. |
||||
* |
||||
* @param <E> property path expression type. |
||||
* @param <P> predicate type. |
||||
*/ |
||||
public interface QueryStrategy<E, P> { |
||||
|
||||
/** |
||||
* Create an expression object from the given {@code property} path. |
||||
* |
||||
* @param property must not be {@literal null}. |
||||
* @return |
||||
*/ |
||||
E createExpression(String property); |
||||
|
||||
/** |
||||
* Create a comparison object according to the {@link Order}. |
||||
* |
||||
* @param order must not be {@literal null}. |
||||
* @param propertyExpression must not be {@literal null}. |
||||
* @param value |
||||
* @return |
||||
*/ |
||||
P compare(Order order, E propertyExpression, Object value); |
||||
|
||||
/** |
||||
* Create an equals-comparison object. |
||||
* |
||||
* @param propertyExpression must not be {@literal null}. |
||||
* @param value |
||||
* @return |
||||
*/ |
||||
P compare(E propertyExpression, @Nullable Object value); |
||||
|
||||
/** |
||||
* AND-combine the {@code intermediate} predicates. |
||||
* |
||||
* @param intermediate |
||||
* @return |
||||
*/ |
||||
P and(List<P> intermediate); |
||||
|
||||
/** |
||||
* OR-combine the {@code intermediate} predicates. |
||||
* |
||||
* @param intermediate |
||||
* @return |
||||
*/ |
||||
P or(List<P> intermediate); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,128 @@
@@ -0,0 +1,128 @@
|
||||
/* |
||||
* Copyright 2023 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.data.jpa.repository.query; |
||||
|
||||
import jakarta.persistence.criteria.CriteriaBuilder; |
||||
import jakarta.persistence.criteria.CriteriaQuery; |
||||
import jakarta.persistence.criteria.Expression; |
||||
import jakarta.persistence.criteria.From; |
||||
import jakarta.persistence.criteria.Predicate; |
||||
import jakarta.persistence.criteria.Root; |
||||
|
||||
import java.util.List; |
||||
|
||||
import org.springframework.data.domain.KeysetScrollPosition; |
||||
import org.springframework.data.domain.Sort; |
||||
import org.springframework.data.domain.Sort.Order; |
||||
import org.springframework.data.jpa.domain.Specification; |
||||
import org.springframework.data.jpa.repository.query.KeysetScrollDelegate.QueryStrategy; |
||||
import org.springframework.data.jpa.repository.support.JpaEntityInformation; |
||||
import org.springframework.data.mapping.PropertyPath; |
||||
import org.springframework.lang.Nullable; |
||||
|
||||
/** |
||||
* {@link Specification} to create scroll queries using keyset-scrolling. |
||||
* |
||||
* @author Mark Paluch |
||||
* @author Christoph Strobl |
||||
* @since 3.1 |
||||
*/ |
||||
public record KeysetScrollSpecification<T> (KeysetScrollPosition position, Sort sort, |
||||
JpaEntityInformation<?, ?> entity) implements Specification<T> { |
||||
|
||||
public KeysetScrollSpecification(KeysetScrollPosition position, Sort sort, JpaEntityInformation<?, ?> entity) { |
||||
|
||||
this.position = position; |
||||
this.entity = entity; |
||||
this.sort = createSort(position, sort, entity); |
||||
} |
||||
|
||||
/** |
||||
* Create a {@link Sort} object to be used with the actual query. |
||||
* |
||||
* @param position must not be {@literal null}. |
||||
* @param sort must not be {@literal null}. |
||||
* @param entity must not be {@literal null}. |
||||
* @return |
||||
*/ |
||||
public static Sort createSort(KeysetScrollPosition position, Sort sort, JpaEntityInformation<?, ?> entity) { |
||||
|
||||
KeysetScrollDelegate delegate = KeysetScrollDelegate.of(position.getDirection()); |
||||
|
||||
Sort sortToUse; |
||||
if (entity.hasCompositeId()) { |
||||
sortToUse = sort.and(Sort.by(entity.getIdAttributeNames().toArray(new String[0]))); |
||||
} else { |
||||
sortToUse = sort.and(Sort.by(entity.getRequiredIdAttribute().getName())); |
||||
} |
||||
|
||||
return delegate.getSortOrders(sortToUse); |
||||
} |
||||
|
||||
@Override |
||||
public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) { |
||||
return createPredicate(root, criteriaBuilder); |
||||
} |
||||
|
||||
@Nullable |
||||
public Predicate createPredicate(Root<?> root, CriteriaBuilder criteriaBuilder) { |
||||
|
||||
KeysetScrollDelegate delegate = KeysetScrollDelegate.of(position.getDirection()); |
||||
return delegate.createPredicate(position, sort, new JpaQueryStrategy(root, criteriaBuilder)); |
||||
} |
||||
|
||||
@SuppressWarnings("rawtypes") |
||||
private static class JpaQueryStrategy implements QueryStrategy<Expression<Comparable>, Predicate> { |
||||
|
||||
private final From<?, ?> from; |
||||
private final CriteriaBuilder cb; |
||||
|
||||
public JpaQueryStrategy(From<?, ?> from, CriteriaBuilder cb) { |
||||
|
||||
this.from = from; |
||||
this.cb = cb; |
||||
} |
||||
|
||||
@Override |
||||
public Expression<Comparable> createExpression(String property) { |
||||
|
||||
PropertyPath path = PropertyPath.from(property, from.getJavaType()); |
||||
return QueryUtils.toExpressionRecursively(from, path); |
||||
} |
||||
|
||||
@Override |
||||
public Predicate compare(Order order, Expression<Comparable> propertyExpression, Object value) { |
||||
|
||||
return order.isAscending() ? cb.greaterThan(propertyExpression, (Comparable) value) |
||||
: cb.lessThan(propertyExpression, (Comparable) value); |
||||
} |
||||
|
||||
@Override |
||||
public Predicate compare(Expression<Comparable> propertyExpression, @Nullable Object value) { |
||||
return value == null ? cb.isNull(propertyExpression) : cb.equal(propertyExpression, value); |
||||
} |
||||
|
||||
@Override |
||||
public Predicate and(List<Predicate> intermediate) { |
||||
return cb.and(intermediate.toArray(new Predicate[0])); |
||||
} |
||||
|
||||
@Override |
||||
public Predicate or(List<Predicate> intermediate) { |
||||
return cb.or(intermediate.toArray(new Predicate[0])); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,105 @@
@@ -0,0 +1,105 @@
|
||||
/* |
||||
* Copyright 2023 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.data.jpa.repository.query; |
||||
|
||||
import jakarta.persistence.Query; |
||||
|
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.function.IntFunction; |
||||
|
||||
import org.springframework.data.domain.KeysetScrollPosition; |
||||
import org.springframework.data.domain.KeysetScrollPosition.Direction; |
||||
import org.springframework.data.domain.OffsetScrollPosition; |
||||
import org.springframework.data.domain.ScrollPosition; |
||||
import org.springframework.data.domain.Sort; |
||||
import org.springframework.data.domain.Sort.Order; |
||||
import org.springframework.data.domain.Window; |
||||
import org.springframework.data.jpa.repository.support.JpaEntityInformation; |
||||
import org.springframework.util.Assert; |
||||
|
||||
/** |
||||
* Delegate to run {@link ScrollPosition scroll queries} and create result {@link Window}. |
||||
* |
||||
* @author Mark Paluch |
||||
* @since 3.1 |
||||
*/ |
||||
public class ScrollDelegate<T> { |
||||
|
||||
private final JpaEntityInformation<T, ?> entity; |
||||
|
||||
protected ScrollDelegate(JpaEntityInformation<T, ?> entity) { |
||||
this.entity = entity; |
||||
} |
||||
|
||||
/** |
||||
* Run the {@link Query} and return a scroll {@link Window}. |
||||
* |
||||
* @param query must not be {@literal null}. |
||||
* @param sort must not be {@literal null}. |
||||
* @param scrollPosition must not be {@literal null}. |
||||
* @return the scroll {@link Window}. |
||||
*/ |
||||
@SuppressWarnings("unchecked") |
||||
public Window<T> scroll(Query query, Sort sort, ScrollPosition scrollPosition) { |
||||
|
||||
Assert.notNull(scrollPosition, "ScrollPosition must not be null"); |
||||
|
||||
int limit = query.getMaxResults(); |
||||
if (limit > 0 && limit != Integer.MAX_VALUE) { |
||||
query = query.setMaxResults(limit + 1); |
||||
} |
||||
|
||||
List<T> result = query.getResultList(); |
||||
|
||||
if (scrollPosition instanceof KeysetScrollPosition keyset) { |
||||
return createWindow(sort, limit, keyset.getDirection(), entity, result); |
||||
} |
||||
|
||||
if (scrollPosition instanceof OffsetScrollPosition offset) { |
||||
return createWindow(result, limit, OffsetScrollPosition.positionFunction(offset.getOffset())); |
||||
} |
||||
|
||||
throw new UnsupportedOperationException("ScrollPosition " + scrollPosition + " not supported"); |
||||
} |
||||
|
||||
private static <T> Window<T> createWindow(Sort sort, int limit, Direction direction, |
||||
JpaEntityInformation<T, ?> entity, List<T> result) { |
||||
|
||||
KeysetScrollDelegate delegate = KeysetScrollDelegate.of(direction); |
||||
List<T> resultsToUse = delegate.postProcessResults(result); |
||||
|
||||
IntFunction<KeysetScrollPosition> positionFunction = value -> { |
||||
|
||||
T object = result.get(value); |
||||
Map<String, Object> keys = entity.getKeyset(sort.stream().map(Order::getProperty).toList(), object); |
||||
|
||||
return KeysetScrollPosition.of(keys); |
||||
}; |
||||
|
||||
return Window.from(delegate.getResultWindow(resultsToUse, limit), positionFunction, hasMoreElements(result, limit)); |
||||
} |
||||
|
||||
private static <T> Window<T> createWindow(List<T> result, int limit, |
||||
IntFunction<? extends ScrollPosition> positionFunction) { |
||||
return Window.from(CollectionUtils.getFirst(limit, result), positionFunction, hasMoreElements(result, limit)); |
||||
} |
||||
|
||||
private static boolean hasMoreElements(List<?> result, int limit) { |
||||
return !result.isEmpty() && result.size() > limit; |
||||
} |
||||
|
||||
} |
||||
@ -1,206 +0,0 @@
@@ -1,206 +0,0 @@
|
||||
/* |
||||
* Copyright 2021-2023 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.data.jpa.repository.support; |
||||
|
||||
import jakarta.persistence.EntityManager; |
||||
import jakarta.persistence.TypedQuery; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.Collection; |
||||
import java.util.Collections; |
||||
import java.util.List; |
||||
import java.util.function.Function; |
||||
import java.util.stream.Stream; |
||||
|
||||
import org.springframework.dao.IncorrectResultSizeDataAccessException; |
||||
import org.springframework.data.domain.Example; |
||||
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.jpa.repository.query.EscapeCharacter; |
||||
import org.springframework.data.jpa.support.PageableUtils; |
||||
import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery; |
||||
import org.springframework.data.support.PageableExecutionUtils; |
||||
import org.springframework.util.Assert; |
||||
|
||||
/** |
||||
* Immutable implementation of {@link FetchableFluentQuery} based on Query by {@link Example}. All methods that return a |
||||
* {@link FetchableFluentQuery} will return a new instance, not the original. |
||||
* |
||||
* @param <S> Domain type |
||||
* @param <R> Result type |
||||
* @author Greg Turnquist |
||||
* @author Mark Paluch |
||||
* @author Jens Schauder |
||||
* @author J.R. Onyschak |
||||
* @since 2.6 |
||||
*/ |
||||
class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<S, R> implements FetchableFluentQuery<R> { |
||||
|
||||
private final Example<S> example; |
||||
private final Function<Sort, TypedQuery<S>> finder; |
||||
private final Function<Example<S>, Long> countOperation; |
||||
private final Function<Example<S>, Boolean> existsOperation; |
||||
private final EntityManager entityManager; |
||||
private final EscapeCharacter escapeCharacter; |
||||
|
||||
public FetchableFluentQueryByExample(Example<S> example, Function<Sort, TypedQuery<S>> finder, |
||||
Function<Example<S>, Long> countOperation, Function<Example<S>, Boolean> existsOperation, |
||||
EntityManager entityManager, EscapeCharacter escapeCharacter) { |
||||
this(example, example.getProbeType(), (Class<R>) example.getProbeType(), Sort.unsorted(), Collections.emptySet(), |
||||
finder, countOperation, existsOperation, entityManager, escapeCharacter); |
||||
} |
||||
|
||||
private FetchableFluentQueryByExample(Example<S> example, Class<S> entityType, Class<R> returnType, Sort sort, |
||||
Collection<String> properties, Function<Sort, TypedQuery<S>> finder, Function<Example<S>, Long> countOperation, |
||||
Function<Example<S>, Boolean> existsOperation, EntityManager entityManager, EscapeCharacter escapeCharacter) { |
||||
|
||||
super(returnType, sort, properties, entityType); |
||||
this.example = example; |
||||
this.finder = finder; |
||||
this.countOperation = countOperation; |
||||
this.existsOperation = existsOperation; |
||||
this.entityManager = entityManager; |
||||
this.escapeCharacter = escapeCharacter; |
||||
} |
||||
|
||||
@Override |
||||
public FetchableFluentQuery<R> sortBy(Sort sort) { |
||||
|
||||
Assert.notNull(sort, "Sort must not be null"); |
||||
|
||||
return new FetchableFluentQueryByExample<>(example, entityType, resultType, this.sort.and(sort), properties, finder, |
||||
countOperation, existsOperation, entityManager, escapeCharacter); |
||||
} |
||||
|
||||
@Override |
||||
public <NR> FetchableFluentQuery<NR> as(Class<NR> resultType) { |
||||
|
||||
Assert.notNull(resultType, "Projection target type must not be null"); |
||||
if (!resultType.isInterface()) { |
||||
throw new UnsupportedOperationException("Class-based DTOs are not yet supported."); |
||||
} |
||||
|
||||
return new FetchableFluentQueryByExample<>(example, entityType, resultType, sort, properties, finder, |
||||
countOperation, existsOperation, entityManager, escapeCharacter); |
||||
} |
||||
|
||||
@Override |
||||
public FetchableFluentQuery<R> project(Collection<String> properties) { |
||||
|
||||
return new FetchableFluentQueryByExample<>(example, entityType, resultType, sort, mergeProperties(properties), |
||||
finder, countOperation, existsOperation, entityManager, escapeCharacter); |
||||
} |
||||
|
||||
@Override |
||||
public R oneValue() { |
||||
|
||||
TypedQuery<S> limitedQuery = createSortedAndProjectedQuery(); |
||||
limitedQuery.setMaxResults(2); // Never need more than 2 values
|
||||
|
||||
List<S> results = limitedQuery.getResultList(); |
||||
|
||||
if (results.size() > 1) { |
||||
throw new IncorrectResultSizeDataAccessException(1); |
||||
} |
||||
|
||||
return results.isEmpty() ? null : getConversionFunction().apply(results.get(0)); |
||||
} |
||||
|
||||
@Override |
||||
public R firstValue() { |
||||
|
||||
TypedQuery<S> limitedQuery = createSortedAndProjectedQuery(); |
||||
limitedQuery.setMaxResults(1); // Never need more than 1 value
|
||||
|
||||
List<S> results = limitedQuery.getResultList(); |
||||
|
||||
return results.isEmpty() ? null : getConversionFunction().apply(results.get(0)); |
||||
} |
||||
|
||||
@Override |
||||
public List<R> all() { |
||||
|
||||
List<S> resultList = createSortedAndProjectedQuery().getResultList(); |
||||
|
||||
return convert(resultList); |
||||
} |
||||
|
||||
@Override |
||||
public Page<R> page(Pageable pageable) { |
||||
return pageable.isUnpaged() ? new PageImpl<>(all()) : readPage(pageable); |
||||
} |
||||
|
||||
@Override |
||||
public Stream<R> stream() { |
||||
|
||||
return createSortedAndProjectedQuery() //
|
||||
.getResultStream() //
|
||||
.map(getConversionFunction()); |
||||
} |
||||
|
||||
@Override |
||||
public long count() { |
||||
return countOperation.apply(example); |
||||
} |
||||
|
||||
@Override |
||||
public boolean exists() { |
||||
return existsOperation.apply(example); |
||||
} |
||||
|
||||
private Page<R> readPage(Pageable pageable) { |
||||
|
||||
TypedQuery<S> pagedQuery = createSortedAndProjectedQuery(); |
||||
|
||||
if (pageable.isPaged()) { |
||||
pagedQuery.setFirstResult(PageableUtils.getOffsetAsInteger(pageable)); |
||||
pagedQuery.setMaxResults(pageable.getPageSize()); |
||||
} |
||||
|
||||
List<R> paginatedResults = convert(pagedQuery.getResultList()); |
||||
|
||||
return PageableExecutionUtils.getPage(paginatedResults, pageable, () -> countOperation.apply(example)); |
||||
} |
||||
|
||||
private TypedQuery<S> createSortedAndProjectedQuery() { |
||||
|
||||
TypedQuery<S> query = finder.apply(sort); |
||||
|
||||
if (!properties.isEmpty()) { |
||||
query.setHint(EntityGraphFactory.HINT, EntityGraphFactory.create(entityManager, entityType, properties)); |
||||
} |
||||
|
||||
return query; |
||||
} |
||||
|
||||
private List<R> convert(List<S> resultList) { |
||||
|
||||
Function<Object, R> conversionFunction = getConversionFunction(); |
||||
List<R> mapped = new ArrayList<>(resultList.size()); |
||||
|
||||
for (S s : resultList) { |
||||
mapped.add(conversionFunction.apply(s)); |
||||
} |
||||
return mapped; |
||||
} |
||||
|
||||
private Function<Object, R> getConversionFunction() { |
||||
return getConversionFunction(example.getProbeType(), resultType); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,46 @@
@@ -0,0 +1,46 @@
|
||||
/* |
||||
* Copyright 2023 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.data.jpa.repository.query; |
||||
|
||||
import static org.assertj.core.api.Assertions.*; |
||||
|
||||
import java.util.List; |
||||
|
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
/** |
||||
* Unit tests for {@link CollectionUtils}. |
||||
* |
||||
* @author Mark Paluch |
||||
*/ |
||||
class CollectionUtilsUnitTests { |
||||
|
||||
@Test // GH-2878
|
||||
void shouldReturnFirstItems() { |
||||
|
||||
assertThat(CollectionUtils.getFirst(2, List.of(1, 2, 3))).containsExactly(1, 2); |
||||
assertThat(CollectionUtils.getFirst(2, List.of(1, 2))).containsExactly(1, 2); |
||||
assertThat(CollectionUtils.getFirst(2, List.of(1))).containsExactly(1); |
||||
} |
||||
|
||||
@Test // GH-2878
|
||||
void shouldReturnLastItems() { |
||||
|
||||
assertThat(CollectionUtils.getLast(2, List.of(1, 2, 3))).containsExactly(2, 3); |
||||
assertThat(CollectionUtils.getLast(2, List.of(1, 2))).containsExactly(1, 2); |
||||
assertThat(CollectionUtils.getLast(2, List.of(1))).containsExactly(1); |
||||
} |
||||
} |
||||
@ -1,43 +0,0 @@
@@ -1,43 +0,0 @@
|
||||
/* |
||||
* Copyright 2022-2023 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.data.jpa.repository.support; |
||||
|
||||
import static org.assertj.core.api.Assertions.*; |
||||
|
||||
import org.junit.jupiter.api.Test; |
||||
import org.springframework.data.domain.Example; |
||||
import org.springframework.data.domain.Sort; |
||||
import org.springframework.data.domain.Sort.Order; |
||||
import org.springframework.data.jpa.repository.support.FetchableFluentQueryByExample; |
||||
|
||||
/** |
||||
* Unit tests for {@link FetchableFluentQueryByExample}. |
||||
* |
||||
* @author J.R. Onyschak |
||||
*/ |
||||
class FetchableFluentQueryByExampleUnitTests { |
||||
|
||||
@Test // GH-2438
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" }) |
||||
void multipleSortBy() { |
||||
|
||||
Sort s1 = Sort.by(Order.by("s1")); |
||||
Sort s2 = Sort.by(Order.by("s2")); |
||||
FetchableFluentQueryByExample f = new FetchableFluentQueryByExample(Example.of(""), null, null, null, null, null); |
||||
f = (FetchableFluentQueryByExample) f.sortBy(s1).sortBy(s2); |
||||
assertThat(f.sort).isEqualTo(s1.and(s2)); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue