Add support for class-based DTOs for Fluent API.
Also, interface-based projections now use Tuple queries to consistently use tuple-based queries. Closes: #2327 Original Pull Request: #3654
This commit is contained in:
committed by
Christoph Strobl
parent
1341c3f14e
commit
e1b76122ae
@@ -305,7 +305,7 @@ public abstract class AbstractJpaQuery implements RepositoryQuery {
|
||||
*/
|
||||
protected abstract Query doCreateCountQuery(JpaParametersParameterAccessor accessor);
|
||||
|
||||
static class TupleConverter implements Converter<Object, Object> {
|
||||
public static class TupleConverter implements Converter<Object, Object> {
|
||||
|
||||
private final ReturnedType type;
|
||||
|
||||
|
||||
@@ -769,7 +769,8 @@ public abstract class QueryUtils {
|
||||
return toExpressionRecursively(from, property, false);
|
||||
}
|
||||
|
||||
static <T> Expression<T> toExpressionRecursively(From<?, ?> from, PropertyPath property, boolean isForSelection) {
|
||||
public static <T> Expression<T> toExpressionRecursively(From<?, ?> from, PropertyPath property,
|
||||
boolean isForSelection) {
|
||||
return toExpressionRecursively(from, property, isForSelection, false);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,17 +16,18 @@
|
||||
package org.springframework.data.jpa.repository.support;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.Query;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.dao.IncorrectResultSizeDataAccessException;
|
||||
import org.springframework.data.domain.KeysetScrollPosition;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
@@ -36,10 +37,18 @@ import org.springframework.data.domain.Window;
|
||||
import org.springframework.data.jpa.repository.query.ScrollDelegate;
|
||||
import org.springframework.data.projection.ProjectionFactory;
|
||||
import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery;
|
||||
import org.springframework.data.repository.query.ReturnedType;
|
||||
import org.springframework.data.support.PageableExecutionUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.querydsl.core.types.EntityPath;
|
||||
import com.querydsl.core.types.Expression;
|
||||
import com.querydsl.core.types.ExpressionBase;
|
||||
import com.querydsl.core.types.Predicate;
|
||||
import com.querydsl.core.types.Visitor;
|
||||
import com.querydsl.core.types.dsl.PathBuilder;
|
||||
import com.querydsl.jpa.JPQLSerializer;
|
||||
import com.querydsl.jpa.impl.AbstractJPAQuery;
|
||||
|
||||
/**
|
||||
@@ -57,33 +66,41 @@ import com.querydsl.jpa.impl.AbstractJPAQuery;
|
||||
*/
|
||||
class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<S, R> implements FetchableFluentQuery<R> {
|
||||
|
||||
private final EntityPath<?> entityPath;
|
||||
private final JpaEntityInformation<S, ?> entityInformation;
|
||||
private final ScrollQueryFactory<AbstractJPAQuery<?, ?>> scrollQueryFactory;
|
||||
private final Predicate predicate;
|
||||
private final Function<Sort, AbstractJPAQuery<?, ?>> finder;
|
||||
|
||||
private final PredicateScrollDelegate<S> scroll;
|
||||
private final BiFunction<Sort, Pageable, AbstractJPAQuery<?, ?>> pagedFinder;
|
||||
private final Function<Predicate, Long> countOperation;
|
||||
private final Function<Predicate, Boolean> existsOperation;
|
||||
private final EntityManager entityManager;
|
||||
|
||||
FetchableFluentQueryByPredicate(Predicate predicate, Class<S> entityType,
|
||||
Function<Sort, AbstractJPAQuery<?, ?>> finder, PredicateScrollDelegate<S> scroll,
|
||||
FetchableFluentQueryByPredicate(EntityPath<?> entityPath, Predicate predicate,
|
||||
JpaEntityInformation<S, ?> entityInformation, Function<Sort, AbstractJPAQuery<?, ?>> finder,
|
||||
ScrollQueryFactory<AbstractJPAQuery<?, ?>> scrollQueryFactory,
|
||||
BiFunction<Sort, Pageable, AbstractJPAQuery<?, ?>> pagedFinder, Function<Predicate, Long> countOperation,
|
||||
Function<Predicate, Boolean> existsOperation, EntityManager entityManager, ProjectionFactory projectionFactory) {
|
||||
this(predicate, entityType, (Class<R>) entityType, Sort.unsorted(), 0, Collections.emptySet(), finder, scroll,
|
||||
this(entityPath, predicate, entityInformation, (Class<R>) entityInformation.getJavaType(), Sort.unsorted(), 0,
|
||||
Collections.emptySet(), finder, scrollQueryFactory,
|
||||
pagedFinder, countOperation, existsOperation, entityManager, projectionFactory);
|
||||
}
|
||||
|
||||
private FetchableFluentQueryByPredicate(Predicate predicate, Class<S> entityType, Class<R> resultType, Sort sort,
|
||||
int limit, Collection<String> properties, Function<Sort, AbstractJPAQuery<?, ?>> finder,
|
||||
PredicateScrollDelegate<S> scroll, BiFunction<Sort, Pageable, AbstractJPAQuery<?, ?>> pagedFinder,
|
||||
private FetchableFluentQueryByPredicate(EntityPath<?> entityPath, Predicate predicate,
|
||||
JpaEntityInformation<S, ?> entityInformation, Class<R> resultType, Sort sort, int limit,
|
||||
Collection<String> properties, Function<Sort, AbstractJPAQuery<?, ?>> finder,
|
||||
ScrollQueryFactory<AbstractJPAQuery<?, ?>> scrollQueryFactory,
|
||||
BiFunction<Sort, Pageable, AbstractJPAQuery<?, ?>> pagedFinder,
|
||||
Function<Predicate, Long> countOperation, Function<Predicate, Boolean> existsOperation,
|
||||
EntityManager entityManager, ProjectionFactory projectionFactory) {
|
||||
|
||||
super(resultType, sort, limit, properties, entityType, projectionFactory);
|
||||
super(resultType, sort, limit, properties, entityInformation.getJavaType(), projectionFactory);
|
||||
this.entityInformation = entityInformation;
|
||||
this.entityPath = entityPath;
|
||||
this.predicate = predicate;
|
||||
this.finder = finder;
|
||||
this.scroll = scroll;
|
||||
this.scrollQueryFactory = scrollQueryFactory;
|
||||
this.pagedFinder = pagedFinder;
|
||||
this.countOperation = countOperation;
|
||||
this.existsOperation = existsOperation;
|
||||
@@ -95,8 +112,9 @@ class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<S, R> imp
|
||||
|
||||
Assert.notNull(sort, "Sort must not be null");
|
||||
|
||||
return new FetchableFluentQueryByPredicate<>(predicate, entityType, resultType, this.sort.and(sort), limit,
|
||||
properties, finder, scroll, pagedFinder, countOperation, existsOperation, entityManager, projectionFactory);
|
||||
return new FetchableFluentQueryByPredicate<>(entityPath, predicate, entityInformation, resultType,
|
||||
this.sort.and(sort), limit, properties, finder, scrollQueryFactory, pagedFinder, countOperation,
|
||||
existsOperation, entityManager, projectionFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -104,8 +122,9 @@ class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<S, R> imp
|
||||
|
||||
Assert.isTrue(limit >= 0, "Limit must not be negative");
|
||||
|
||||
return new FetchableFluentQueryByPredicate<>(predicate, entityType, resultType, sort, limit, properties, finder,
|
||||
scroll, pagedFinder, countOperation, existsOperation, entityManager, projectionFactory);
|
||||
return new FetchableFluentQueryByPredicate<>(entityPath, predicate, entityInformation, resultType, sort, limit,
|
||||
properties, finder, scrollQueryFactory, pagedFinder, countOperation, existsOperation, entityManager,
|
||||
projectionFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -113,19 +132,17 @@ class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<S, R> imp
|
||||
|
||||
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 FetchableFluentQueryByPredicate<>(predicate, entityType, resultType, sort, limit, properties, finder,
|
||||
scroll, pagedFinder, countOperation, existsOperation, entityManager, projectionFactory);
|
||||
return new FetchableFluentQueryByPredicate<>(entityPath, predicate, entityInformation, resultType, sort, limit,
|
||||
properties, finder, scrollQueryFactory, pagedFinder, countOperation, existsOperation, entityManager,
|
||||
projectionFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FetchableFluentQuery<R> project(Collection<String> properties) {
|
||||
|
||||
return new FetchableFluentQueryByPredicate<>(predicate, entityType, resultType, sort, limit,
|
||||
mergeProperties(properties), finder, scroll, pagedFinder, countOperation, existsOperation, entityManager,
|
||||
return new FetchableFluentQueryByPredicate<>(entityPath, predicate, entityInformation, resultType, sort, limit,
|
||||
mergeProperties(properties), finder, scrollQueryFactory, pagedFinder, countOperation, existsOperation,
|
||||
entityManager,
|
||||
projectionFactory);
|
||||
}
|
||||
|
||||
@@ -163,7 +180,8 @@ class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<S, R> imp
|
||||
|
||||
Assert.notNull(scrollPosition, "ScrollPosition must not be null");
|
||||
|
||||
return scroll.scroll(sort, limit, scrollPosition).map(getConversionFunction());
|
||||
return new PredicateScrollDelegate<>(scrollQueryFactory, entityInformation)
|
||||
.scroll(returnedType, sort, limit, scrollPosition).map(getConversionFunction());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -192,6 +210,35 @@ class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<S, R> imp
|
||||
private AbstractJPAQuery<?, ?> createSortedAndProjectedQuery() {
|
||||
|
||||
AbstractJPAQuery<?, ?> query = finder.apply(sort);
|
||||
applyQuerySettings(this.returnedType, this.limit, query, null);
|
||||
return query;
|
||||
}
|
||||
|
||||
private void applyQuerySettings(ReturnedType returnedType, int limit, AbstractJPAQuery<?, ?> query,
|
||||
@Nullable ScrollPosition scrollPosition) {
|
||||
|
||||
List<String> inputProperties = returnedType.getInputProperties();
|
||||
|
||||
if (returnedType.needsCustomConstruction() && !inputProperties.isEmpty()) {
|
||||
|
||||
Collection<String> requiredSelection;
|
||||
if (scrollPosition instanceof KeysetScrollPosition && returnedType.getReturnedType().isInterface()) {
|
||||
requiredSelection = new LinkedHashSet<>(inputProperties);
|
||||
sort.forEach(it -> requiredSelection.add(it.getProperty()));
|
||||
entityInformation.getIdAttributeNames().forEach(requiredSelection::add);
|
||||
} else {
|
||||
requiredSelection = inputProperties;
|
||||
}
|
||||
|
||||
PathBuilder<?> builder = new PathBuilder<>(entityPath.getType(), entityPath.getMetadata());
|
||||
Expression<?>[] projection = requiredSelection.stream().map(builder::get).toArray(Expression[]::new);
|
||||
|
||||
if (returnedType.getReturnedType().isInterface()) {
|
||||
query.select(new JakartaTuple(projection));
|
||||
} else {
|
||||
query.select(new DtoProjection(returnedType.getReturnedType(), projection));
|
||||
}
|
||||
}
|
||||
|
||||
if (!properties.isEmpty()) {
|
||||
query.setHint(EntityGraphFactory.HINT, EntityGraphFactory.create(entityManager, entityType, properties));
|
||||
@@ -200,8 +247,6 @@ class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<S, R> imp
|
||||
if (limit != 0) {
|
||||
query.limit(limit);
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
private Page<R> readPage(Pageable pageable) {
|
||||
@@ -233,23 +278,57 @@ class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<S, R> imp
|
||||
return getConversionFunction(entityType, resultType);
|
||||
}
|
||||
|
||||
static class PredicateScrollDelegate<T> extends ScrollDelegate<T> {
|
||||
class PredicateScrollDelegate<T> extends ScrollDelegate<T> {
|
||||
|
||||
private final ScrollQueryFactory scrollFunction;
|
||||
private final ScrollQueryFactory<AbstractJPAQuery<?, ?>> scrollFunction;
|
||||
|
||||
PredicateScrollDelegate(ScrollQueryFactory scrollQueryFactory, JpaEntityInformation<T, ?> entity) {
|
||||
PredicateScrollDelegate(ScrollQueryFactory<AbstractJPAQuery<?, ?>> scrollQueryFactory,
|
||||
JpaEntityInformation<T, ?> entity) {
|
||||
super(entity);
|
||||
this.scrollFunction = scrollQueryFactory;
|
||||
}
|
||||
|
||||
public Window<T> scroll(Sort sort, int limit, ScrollPosition scrollPosition) {
|
||||
public Window<T> scroll(ReturnedType returnedType, Sort sort, int limit, ScrollPosition scrollPosition) {
|
||||
|
||||
Query query = scrollFunction.createQuery(sort, scrollPosition);
|
||||
if (limit > 0) {
|
||||
query = query.setMaxResults(limit);
|
||||
}
|
||||
return scroll(query, sort, scrollPosition);
|
||||
AbstractJPAQuery<?, ?> query = scrollFunction.createQuery(returnedType, sort, scrollPosition);
|
||||
|
||||
applyQuerySettings(returnedType, limit, query, scrollPosition);
|
||||
|
||||
return scroll(query.createQuery(), sort, scrollPosition);
|
||||
}
|
||||
}
|
||||
|
||||
private static class DtoProjection extends ExpressionBase<Object> {
|
||||
|
||||
private final Expression<?>[] projection;
|
||||
|
||||
public DtoProjection(Class<?> resultType, Expression<?>[] projection) {
|
||||
super(resultType);
|
||||
this.projection = projection;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <R, C> R accept(Visitor<R, C> v, @Nullable C context) {
|
||||
|
||||
if (v instanceof JPQLSerializer s) {
|
||||
|
||||
s.append("new ").append(getType().getName()).append("(");
|
||||
boolean first = true;
|
||||
for (Expression<?> expression : projection) {
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
s.append(", ");
|
||||
}
|
||||
|
||||
expression.accept(v, context);
|
||||
}
|
||||
|
||||
s.append(")");
|
||||
}
|
||||
|
||||
return (R) this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -38,6 +39,7 @@ import org.springframework.data.jpa.repository.query.ScrollDelegate;
|
||||
import org.springframework.data.jpa.support.PageableUtils;
|
||||
import org.springframework.data.projection.ProjectionFactory;
|
||||
import org.springframework.data.repository.query.FluentQuery;
|
||||
import org.springframework.data.repository.query.ReturnedType;
|
||||
import org.springframework.data.support.PageableExecutionUtils;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -55,13 +57,14 @@ class FetchableFluentQueryBySpecification<S, R> extends FluentQuerySupport<S, R>
|
||||
implements FluentQuery.FetchableFluentQuery<R> {
|
||||
|
||||
private final Specification<S> spec;
|
||||
private final Function<Sort, TypedQuery<S>> finder;
|
||||
private final BiFunction<ReturnedType, Sort, TypedQuery<S>> finder;
|
||||
private final SpecificationScrollDelegate<S> scroll;
|
||||
private final Function<Specification<S>, Long> countOperation;
|
||||
private final Function<Specification<S>, Boolean> existsOperation;
|
||||
private final EntityManager entityManager;
|
||||
|
||||
FetchableFluentQueryBySpecification(Specification<S> spec, Class<S> entityType, Function<Sort, TypedQuery<S>> finder,
|
||||
FetchableFluentQueryBySpecification(Specification<S> spec, Class<S> entityType,
|
||||
BiFunction<ReturnedType, Sort, TypedQuery<S>> finder,
|
||||
SpecificationScrollDelegate<S> scrollDelegate, Function<Specification<S>, Long> countOperation,
|
||||
Function<Specification<S>, Boolean> existsOperation, EntityManager entityManager,
|
||||
ProjectionFactory projectionFactory) {
|
||||
@@ -70,7 +73,7 @@ class FetchableFluentQueryBySpecification<S, R> extends FluentQuerySupport<S, R>
|
||||
}
|
||||
|
||||
private FetchableFluentQueryBySpecification(Specification<S> spec, Class<S> entityType, Class<R> resultType,
|
||||
Sort sort, int limit, Collection<String> properties, Function<Sort, TypedQuery<S>> finder,
|
||||
Sort sort, int limit, Collection<String> properties, BiFunction<ReturnedType, Sort, TypedQuery<S>> finder,
|
||||
SpecificationScrollDelegate<S> scrollDelegate, Function<Specification<S>, Long> countOperation,
|
||||
Function<Specification<S>, Boolean> existsOperation, EntityManager entityManager,
|
||||
ProjectionFactory projectionFactory) {
|
||||
@@ -106,9 +109,6 @@ class FetchableFluentQueryBySpecification<S, R> extends FluentQuerySupport<S, R>
|
||||
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 FetchableFluentQueryBySpecification<>(spec, entityType, resultType, sort, limit, properties, finder,
|
||||
scroll, countOperation, existsOperation, entityManager, projectionFactory);
|
||||
@@ -155,7 +155,7 @@ class FetchableFluentQueryBySpecification<S, R> extends FluentQuerySupport<S, R>
|
||||
|
||||
Assert.notNull(scrollPosition, "ScrollPosition must not be null");
|
||||
|
||||
return scroll.scroll(sort, limit, scrollPosition).map(getConversionFunction());
|
||||
return scroll.scroll(returnedType, sort, limit, scrollPosition).map(getConversionFunction());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -183,7 +183,7 @@ class FetchableFluentQueryBySpecification<S, R> extends FluentQuerySupport<S, R>
|
||||
|
||||
private TypedQuery<S> createSortedAndProjectedQuery() {
|
||||
|
||||
TypedQuery<S> query = finder.apply(sort);
|
||||
TypedQuery<S> query = finder.apply(returnedType, sort);
|
||||
|
||||
if (!properties.isEmpty()) {
|
||||
query.setHint(EntityGraphFactory.HINT, EntityGraphFactory.create(entityManager, entityType, properties));
|
||||
@@ -227,16 +227,17 @@ class FetchableFluentQueryBySpecification<S, R> extends FluentQuerySupport<S, R>
|
||||
|
||||
static class SpecificationScrollDelegate<T> extends ScrollDelegate<T> {
|
||||
|
||||
private final ScrollQueryFactory scrollFunction;
|
||||
private final ScrollQueryFactory<TypedQuery<T>> scrollFunction;
|
||||
|
||||
SpecificationScrollDelegate(ScrollQueryFactory scrollQueryFactory, JpaEntityInformation<T, ?> entity) {
|
||||
SpecificationScrollDelegate(ScrollQueryFactory<TypedQuery<T>> scrollQueryFactory,
|
||||
JpaEntityInformation<T, ?> entity) {
|
||||
super(entity);
|
||||
this.scrollFunction = scrollQueryFactory;
|
||||
}
|
||||
|
||||
public Window<T> scroll(Sort sort, int limit, ScrollPosition scrollPosition) {
|
||||
public Window<T> scroll(ReturnedType returnedType, Sort sort, int limit, ScrollPosition scrollPosition) {
|
||||
|
||||
Query query = scrollFunction.createQuery(sort, scrollPosition);
|
||||
Query query = scrollFunction.createQuery(returnedType, sort, scrollPosition);
|
||||
|
||||
if (limit > 0) {
|
||||
query = query.setMaxResults(limit);
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
*/
|
||||
package org.springframework.data.jpa.repository.support;
|
||||
|
||||
import jakarta.persistence.Query;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
@@ -26,7 +24,9 @@ import java.util.function.Function;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.data.domain.ScrollPosition;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.jpa.repository.query.AbstractJpaQuery;
|
||||
import org.springframework.data.projection.ProjectionFactory;
|
||||
import org.springframework.data.repository.query.ReturnedType;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
@@ -41,6 +41,7 @@ import org.springframework.lang.Nullable;
|
||||
*/
|
||||
abstract class FluentQuerySupport<S, R> {
|
||||
|
||||
protected final ReturnedType returnedType;
|
||||
protected final Class<R> resultType;
|
||||
protected final Sort sort;
|
||||
protected final int limit;
|
||||
@@ -51,6 +52,7 @@ abstract class FluentQuerySupport<S, R> {
|
||||
FluentQuerySupport(Class<R> resultType, Sort sort, int limit, @Nullable Collection<String> properties,
|
||||
Class<S> entityType, ProjectionFactory projectionFactory) {
|
||||
|
||||
this.returnedType = ReturnedType.of(resultType, entityType, projectionFactory);
|
||||
this.resultType = resultType;
|
||||
this.sort = sort;
|
||||
this.limit = limit;
|
||||
@@ -80,15 +82,20 @@ abstract class FluentQuerySupport<S, R> {
|
||||
return (Function<Object, R>) Function.identity();
|
||||
}
|
||||
|
||||
if (targetType.isInterface()) {
|
||||
return o -> projectionFactory.createProjection(targetType, o);
|
||||
if (returnedType.isProjecting()) {
|
||||
|
||||
AbstractJpaQuery.TupleConverter tupleConverter = new AbstractJpaQuery.TupleConverter(returnedType);
|
||||
|
||||
if (resultType.isInterface()) {
|
||||
return o -> projectionFactory.createProjection(targetType, tupleConverter.convert(o));
|
||||
}
|
||||
}
|
||||
|
||||
return o -> DefaultConversionService.getSharedInstance().convert(o, targetType);
|
||||
}
|
||||
|
||||
interface ScrollQueryFactory {
|
||||
Query createQuery(Sort sort, ScrollPosition scrollPosition);
|
||||
interface ScrollQueryFactory<Q> {
|
||||
Q createQuery(ReturnedType returnedType, Sort sort, ScrollPosition scrollPosition);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2024 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.Tuple;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import com.querydsl.core.types.Expression;
|
||||
import com.querydsl.core.types.ExpressionBase;
|
||||
import com.querydsl.core.types.ExpressionUtils;
|
||||
import com.querydsl.core.types.FactoryExpression;
|
||||
import com.querydsl.core.types.Ops;
|
||||
import com.querydsl.core.types.Path;
|
||||
import com.querydsl.core.types.Projections;
|
||||
import com.querydsl.core.types.Visitor;
|
||||
import com.querydsl.jpa.JPQLSerializer;
|
||||
|
||||
class JakartaTuple extends ExpressionBase<Tuple> {
|
||||
|
||||
private final List<Expression<?>> args;
|
||||
|
||||
/**
|
||||
* Create a new JakartaTuple instance
|
||||
*
|
||||
* @param args
|
||||
*/
|
||||
protected JakartaTuple(Expression<?>... args) {
|
||||
this(Arrays.asList(args));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new JakartaTuple instance
|
||||
*
|
||||
* @param args
|
||||
*/
|
||||
protected JakartaTuple(List<Expression<?>> args) {
|
||||
super(Tuple.class);
|
||||
this.args = args.stream().map(it -> {
|
||||
|
||||
if (it instanceof Path<?> p) {
|
||||
return ExpressionUtils.operation(p.getType(), Ops.ALIAS, p, p);
|
||||
}
|
||||
|
||||
return it;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <R, C> R accept(Visitor<R, C> v, C context) {
|
||||
|
||||
if (v instanceof JPQLSerializer) {
|
||||
return Projections.tuple(args).accept(v, context);
|
||||
}
|
||||
|
||||
return (R) this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == this) {
|
||||
return true;
|
||||
} else if (obj instanceof FactoryExpression) {
|
||||
FactoryExpression<?> c = (FactoryExpression<?>) obj;
|
||||
return args.equals(c.getArgs()) && getType().equals(c.getType());
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -161,7 +161,9 @@ public class JpaMetamodelEntityInformation<T, ID> extends JpaEntityInformationSu
|
||||
return (ID) t.get(idMetadata.getSimpleIdAttribute().getName());
|
||||
}
|
||||
|
||||
return (ID) persistenceUnitUtil.getIdentifier(entity);
|
||||
if (getJavaType().isInstance(entity)) {
|
||||
return (ID) persistenceUnitUtil.getIdentifier(entity);
|
||||
}
|
||||
}
|
||||
|
||||
// otherwise, check if the complex id type has any partially filled fields
|
||||
@@ -172,6 +174,10 @@ public class JpaMetamodelEntityInformation<T, ID> extends JpaEntityInformationSu
|
||||
|
||||
Object propertyValue = entityWrapper.getPropertyValue(attribute.getName());
|
||||
|
||||
if (idMetadata.hasSimpleId()) {
|
||||
return (ID) propertyValue;
|
||||
}
|
||||
|
||||
if (propertyValue != null) {
|
||||
partialIdValueFound = true;
|
||||
}
|
||||
|
||||
@@ -78,9 +78,9 @@ public class Querydsl {
|
||||
public <T> AbstractJPAQuery<T, JPAQuery<T>> createQuery() {
|
||||
|
||||
return switch (provider) {
|
||||
case ECLIPSELINK -> new JPAQuery<>(em, EclipseLinkTemplates.DEFAULT);
|
||||
case HIBERNATE -> new JPAQuery<>(em, HQLTemplates.DEFAULT);
|
||||
default -> new JPAQuery<>(em);
|
||||
case ECLIPSELINK -> new SpringDataJpaQuery<>(em, EclipseLinkTemplates.DEFAULT);
|
||||
case HIBERNATE -> new SpringDataJpaQuery<>(em, HQLTemplates.DEFAULT);
|
||||
default -> new SpringDataJpaQuery<>(em);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@ import org.springframework.data.jpa.repository.EntityGraph;
|
||||
import org.springframework.data.jpa.repository.query.KeysetScrollDelegate;
|
||||
import org.springframework.data.jpa.repository.query.KeysetScrollDelegate.QueryStrategy;
|
||||
import org.springframework.data.jpa.repository.query.KeysetScrollSpecification;
|
||||
import org.springframework.data.jpa.repository.support.FetchableFluentQueryByPredicate.PredicateScrollDelegate;
|
||||
import org.springframework.data.jpa.repository.support.FluentQuerySupport.ScrollQueryFactory;
|
||||
import org.springframework.data.projection.ProjectionFactory;
|
||||
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
|
||||
@@ -194,7 +193,7 @@ public class QuerydslJpaPredicateExecutor<T> implements QuerydslPredicateExecuto
|
||||
return select;
|
||||
};
|
||||
|
||||
ScrollQueryFactory scroll = (sort, scrollPosition) -> {
|
||||
ScrollQueryFactory<AbstractJPAQuery<?, ?>> scroll = (returnedType, sort, scrollPosition) -> {
|
||||
|
||||
Predicate predicateToUse = predicate;
|
||||
|
||||
@@ -220,7 +219,7 @@ public class QuerydslJpaPredicateExecutor<T> implements QuerydslPredicateExecuto
|
||||
}
|
||||
}
|
||||
|
||||
return select.createQuery();
|
||||
return select;
|
||||
};
|
||||
|
||||
BiFunction<Sort, Pageable, AbstractJPAQuery<?, ?>> pagedFinder = (sort, pageable) -> {
|
||||
@@ -235,10 +234,11 @@ public class QuerydslJpaPredicateExecutor<T> implements QuerydslPredicateExecuto
|
||||
};
|
||||
|
||||
FetchableFluentQueryByPredicate<T, T> fluentQuery = new FetchableFluentQueryByPredicate<>( //
|
||||
path,
|
||||
predicate, //
|
||||
this.entityInformation.getJavaType(), //
|
||||
this.entityInformation, //
|
||||
finder, //
|
||||
new PredicateScrollDelegate<>(scroll, entityInformation), //
|
||||
scroll, //
|
||||
pagedFinder, //
|
||||
this::count, //
|
||||
this::exists, //
|
||||
|
||||
@@ -30,16 +30,19 @@ import jakarta.persistence.criteria.ParameterExpression;
|
||||
import jakarta.persistence.criteria.Path;
|
||||
import jakarta.persistence.criteria.Predicate;
|
||||
import jakarta.persistence.criteria.Root;
|
||||
import jakarta.persistence.criteria.Selection;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.data.domain.Example;
|
||||
@@ -48,6 +51,7 @@ import org.springframework.data.domain.OffsetScrollPosition;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.ScrollPosition;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.jpa.convert.QueryByExamplePredicateBuilder;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
@@ -60,9 +64,11 @@ import org.springframework.data.jpa.repository.support.FetchableFluentQueryBySpe
|
||||
import org.springframework.data.jpa.repository.support.FluentQuerySupport.ScrollQueryFactory;
|
||||
import org.springframework.data.jpa.repository.support.QueryHints.NoHints;
|
||||
import org.springframework.data.jpa.support.PageableUtils;
|
||||
import org.springframework.data.mapping.PropertyPath;
|
||||
import org.springframework.data.projection.ProjectionFactory;
|
||||
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
|
||||
import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery;
|
||||
import org.springframework.data.repository.query.ReturnedType;
|
||||
import org.springframework.data.support.PageableExecutionUtils;
|
||||
import org.springframework.data.util.ProxyUtils;
|
||||
import org.springframework.data.util.Streamable;
|
||||
@@ -111,7 +117,7 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
|
||||
private final PersistenceProvider provider;
|
||||
|
||||
private @Nullable CrudMethodMetadata metadata;
|
||||
private @Nullable ProjectionFactory projectionFactory;
|
||||
private ProjectionFactory projectionFactory;
|
||||
private EscapeCharacter escapeCharacter = EscapeCharacter.DEFAULT;
|
||||
|
||||
/**
|
||||
@@ -128,6 +134,7 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
|
||||
this.entityInformation = entityInformation;
|
||||
this.entityManager = entityManager;
|
||||
this.provider = PersistenceProvider.fromEntityManager(entityManager);
|
||||
this.projectionFactory = new SpelAwareProxyProjectionFactory();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -506,7 +513,7 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
|
||||
Assert.notNull(spec, SPECIFICATION_MUST_NOT_BE_NULL);
|
||||
Assert.notNull(queryFunction, QUERY_FUNCTION_MUST_NOT_BE_NULL);
|
||||
|
||||
ScrollQueryFactory scrollFunction = (sort, scrollPosition) -> {
|
||||
ScrollQueryFactory<TypedQuery<T>> scrollFunction = (returnedType, sort, scrollPosition) -> {
|
||||
|
||||
Specification<T> specToUse = spec;
|
||||
|
||||
@@ -516,7 +523,7 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
|
||||
specToUse = specToUse.and(keysetSpec);
|
||||
}
|
||||
|
||||
TypedQuery<T> query = getQuery(specToUse, domainClass, sort);
|
||||
TypedQuery<T> query = getQuery(returnedType, specToUse, domainClass, sort, scrollPosition);
|
||||
|
||||
if (scrollPosition instanceof OffsetScrollPosition offset) {
|
||||
if (!offset.isInitial()) {
|
||||
@@ -527,7 +534,8 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
|
||||
return query;
|
||||
};
|
||||
|
||||
Function<Sort, TypedQuery<T>> finder = sort -> getQuery(spec, domainClass, sort);
|
||||
BiFunction<ReturnedType, Sort, TypedQuery<T>> finder = (returnedType, sort) -> getQuery(returnedType, spec,
|
||||
domainClass, sort, null);
|
||||
|
||||
SpecificationScrollDelegate<T> scrollDelegate = new SpecificationScrollDelegate<>(scrollFunction,
|
||||
entityInformation);
|
||||
@@ -749,12 +757,63 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
|
||||
* @param sort must not be {@literal null}.
|
||||
*/
|
||||
protected <S extends T> TypedQuery<S> getQuery(@Nullable Specification<S> spec, Class<S> domainClass, Sort sort) {
|
||||
return getQuery(ReturnedType.of(domainClass, domainClass, projectionFactory), spec, domainClass, sort, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link TypedQuery} for the given {@link Specification} and {@link Sort}.
|
||||
*
|
||||
* @param returnedType must not be {@literal null}.
|
||||
* @param spec can be {@literal null}.
|
||||
* @param domainClass must not be {@literal null}.
|
||||
* @param sort must not be {@literal null}.
|
||||
*/
|
||||
private <S extends T> TypedQuery<S> getQuery(ReturnedType returnedType, @Nullable Specification<S> spec,
|
||||
Class<S> domainClass, Sort sort, @Nullable ScrollPosition scrollPosition) {
|
||||
|
||||
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
|
||||
CriteriaQuery<S> query = builder.createQuery(domainClass);
|
||||
CriteriaQuery<S> query;
|
||||
|
||||
List<String> inputProperties = returnedType.getInputProperties();
|
||||
|
||||
if (returnedType.needsCustomConstruction() && !inputProperties.isEmpty()) {
|
||||
query = (CriteriaQuery) (returnedType.getReturnedType().isInterface() ? builder.createTupleQuery()
|
||||
: builder.createQuery(returnedType.getReturnedType()));
|
||||
} else {
|
||||
query = builder.createQuery(domainClass);
|
||||
}
|
||||
|
||||
Root<S> root = applySpecificationToCriteria(spec, domainClass, query);
|
||||
query.select(root);
|
||||
|
||||
if (returnedType.needsCustomConstruction() && !inputProperties.isEmpty()) {
|
||||
|
||||
Collection<String> requiredSelection;
|
||||
|
||||
if (scrollPosition instanceof KeysetScrollPosition && returnedType.getReturnedType().isInterface()) {
|
||||
requiredSelection = new LinkedHashSet<>(inputProperties);
|
||||
sort.stream().map(Sort.Order::getProperty).forEach(requiredSelection::add);
|
||||
entityInformation.getIdAttributeNames().forEach(requiredSelection::add);
|
||||
} else {
|
||||
requiredSelection = inputProperties;
|
||||
}
|
||||
|
||||
List<Selection<?>> selections = new ArrayList<>();
|
||||
|
||||
for (String property : requiredSelection) {
|
||||
|
||||
PropertyPath path = PropertyPath.from(property, returnedType.getDomainType());
|
||||
selections.add(QueryUtils.toExpressionRecursively(root, path, true).alias(property));
|
||||
}
|
||||
|
||||
Class<?> typeToRead = returnedType.getReturnedType();
|
||||
|
||||
query = typeToRead.isInterface() //
|
||||
? query.multiselect(selections) //
|
||||
: query.select((Selection) builder.construct(typeToRead, //
|
||||
selections.toArray(new Selection[0])));
|
||||
} else {
|
||||
query.select(root);
|
||||
}
|
||||
|
||||
if (sort.isSorted()) {
|
||||
query.orderBy(toOrders(sort, root, builder));
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2024 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.Query;
|
||||
import jakarta.persistence.Tuple;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import com.querydsl.core.QueryModifiers;
|
||||
import com.querydsl.core.types.Expression;
|
||||
import com.querydsl.core.types.FactoryExpression;
|
||||
import com.querydsl.jpa.JPQLSerializer;
|
||||
import com.querydsl.jpa.JPQLTemplates;
|
||||
import com.querydsl.jpa.impl.JPAQuery;
|
||||
import com.querydsl.jpa.impl.JPAUtil;
|
||||
|
||||
/**
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
class SpringDataJpaQuery<T> extends JPAQuery<T> {
|
||||
|
||||
public SpringDataJpaQuery(EntityManager em) {
|
||||
super(em);
|
||||
}
|
||||
|
||||
public SpringDataJpaQuery(EntityManager em, JPQLTemplates templates) {
|
||||
super(em, templates);
|
||||
}
|
||||
|
||||
protected Query createQuery(@Nullable QueryModifiers modifiers, boolean forCount) {
|
||||
|
||||
JPQLSerializer serializer = serialize(forCount);
|
||||
String queryString = serializer.toString();
|
||||
logQuery(queryString);
|
||||
|
||||
Query query = getMetadata().getProjection() instanceof JakartaTuple
|
||||
? entityManager.createQuery(queryString, Tuple.class)
|
||||
: entityManager.createQuery(queryString);
|
||||
|
||||
JPAUtil.setConstants(query, serializer.getConstants(), getMetadata().getParams());
|
||||
if (modifiers != null && modifiers.isRestricting()) {
|
||||
Integer limit = modifiers.getLimitAsInteger();
|
||||
Integer offset = modifiers.getOffsetAsInteger();
|
||||
if (limit != null) {
|
||||
query.setMaxResults(limit);
|
||||
}
|
||||
if (offset != null) {
|
||||
query.setFirstResult(offset);
|
||||
}
|
||||
}
|
||||
if (lockMode != null) {
|
||||
query.setLockMode(lockMode);
|
||||
}
|
||||
if (flushMode != null) {
|
||||
query.setFlushMode(flushMode);
|
||||
}
|
||||
|
||||
for (Map.Entry<String, Object> entry : hints.entrySet()) {
|
||||
query.setHint(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
// set transformer, if necessary and possible
|
||||
Expression<?> projection = getMetadata().getProjection();
|
||||
this.projection = null; // necessary when query is reused
|
||||
|
||||
if (!forCount && projection instanceof FactoryExpression) {
|
||||
if (!queryHandler.transform(query, (FactoryExpression<?>) projection)) {
|
||||
this.projection = (FactoryExpression) projection;
|
||||
}
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1410,6 +1410,57 @@ class UserRepositoryTests {
|
||||
assertThat(previousWindow.hasNext()).isFalse();
|
||||
}
|
||||
|
||||
@Test // GH-2327
|
||||
void scrollByPredicateKeysetWithInterfaceProjection() {
|
||||
|
||||
User jane1 = new User("Jane", "Doe", "jane@doe1.com");
|
||||
User jane2 = new User("Jane", "Doe", "jane@doe2.com");
|
||||
User john1 = new User("John", "Doe", "john@doe1.com");
|
||||
User john2 = new User("John", "Doe", "john@doe2.com");
|
||||
|
||||
repository.saveAllAndFlush(Arrays.asList(john1, john2, jane1, jane2));
|
||||
|
||||
Window<UserProjectionInterfaceBased> firstWindow = repository.findBy(QUser.user.firstname.startsWith("J"),
|
||||
q -> q.limit(1).sortBy(Sort.by("firstname", "emailAddress")).as(UserProjectionInterfaceBased.class)
|
||||
.scroll(ScrollPosition.keyset()));
|
||||
|
||||
assertThat(firstWindow.getContent()).extracting(UserProjectionInterfaceBased::getFirstname)
|
||||
.containsOnly(jane1.getFirstname());
|
||||
assertThat(firstWindow.hasNext()).isTrue();
|
||||
|
||||
Window<UserProjectionInterfaceBased> nextWindow = repository.findBy(QUser.user.firstname.startsWith("J"),
|
||||
q -> q.limit(2).sortBy(Sort.by("firstname", "emailAddress")).as(UserProjectionInterfaceBased.class)
|
||||
.scroll(firstWindow.positionAt(0)));
|
||||
|
||||
assertThat(nextWindow.getContent()).extracting(UserProjectionInterfaceBased::getFirstname)
|
||||
.containsExactly(jane2.getFirstname(), john1.getFirstname());
|
||||
assertThat(nextWindow.hasNext()).isTrue();
|
||||
}
|
||||
|
||||
@Test // GH-2327
|
||||
void scrollByPredicateKeysetWithDtoProjection() {
|
||||
|
||||
User jane1 = new User("Jane", "Doe", "jane@doe1.com");
|
||||
User jane2 = new User("Jane", "Doe", "jane@doe2.com");
|
||||
User john1 = new User("John", "Doe", "john@doe1.com");
|
||||
User john2 = new User("John", "Doe", "john@doe2.com");
|
||||
|
||||
repository.saveAllAndFlush(Arrays.asList(john1, john2, jane1, jane2));
|
||||
|
||||
Window<UserDto> firstWindow = repository.findBy(QUser.user.firstname.startsWith("J"),
|
||||
q -> q.limit(1).sortBy(Sort.by("firstname", "emailAddress")).as(UserDto.class).scroll(ScrollPosition.keyset()));
|
||||
|
||||
assertThat(firstWindow.getContent()).extracting(UserDto::firstname).containsOnly(jane1.getFirstname());
|
||||
assertThat(firstWindow.hasNext()).isTrue();
|
||||
|
||||
Window<UserDto> nextWindow = repository.findBy(QUser.user.firstname.startsWith("J"), q -> q.limit(2)
|
||||
.sortBy(Sort.by("firstname", "emailAddress")).as(UserDto.class).scroll(firstWindow.positionAt(0)));
|
||||
|
||||
assertThat(nextWindow.getContent()).extracting(UserDto::firstname).containsExactly(jane2.getFirstname(),
|
||||
john1.getFirstname());
|
||||
assertThat(nextWindow.hasNext()).isTrue();
|
||||
}
|
||||
|
||||
@Test // GH-2878
|
||||
void scrollByPartTreeKeysetBackward() {
|
||||
|
||||
@@ -2557,40 +2608,6 @@ class UserRepositoryTests {
|
||||
.containsExactlyInAnyOrder(thirdUser.getFirstname(), firstUser.getFirstname(), fourthUser.getFirstname());
|
||||
}
|
||||
|
||||
@Test // GH-2294
|
||||
void fluentExamplesWithClassBasedDtosNotYetSupported() {
|
||||
|
||||
class UserDto {
|
||||
String firstname;
|
||||
|
||||
public UserDto() {}
|
||||
|
||||
public String getFirstname() {
|
||||
return this.firstname;
|
||||
}
|
||||
|
||||
public void setFirstname(String firstname) {
|
||||
this.firstname = firstname;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "UserDto(firstname=" + this.getFirstname() + ")";
|
||||
}
|
||||
}
|
||||
|
||||
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> {
|
||||
|
||||
User prototype = new User();
|
||||
prototype.setFirstname("v");
|
||||
|
||||
repository.findBy(
|
||||
of(prototype,
|
||||
matching().withIgnorePaths("age", "createdAt", "active").withMatcher("firstname",
|
||||
GenericPropertyMatcher::contains)), //
|
||||
q -> q.as(UserDto.class).sortBy(Sort.by("firstname")).all());
|
||||
});
|
||||
}
|
||||
|
||||
@Test // GH-2294
|
||||
void countByFluentExample() {
|
||||
|
||||
@@ -2692,6 +2709,17 @@ class UserRepositoryTests {
|
||||
.containsExactlyInAnyOrder(firstUser.getFirstname(), thirdUser.getFirstname(), fourthUser.getFirstname());
|
||||
}
|
||||
|
||||
@Test // GH-2327
|
||||
void findByFluentSpecificationWithDtoProjection() {
|
||||
|
||||
flushTestUsers();
|
||||
|
||||
List<UserDto> users = repository.findBy(userHasFirstnameLike("v"), q -> q.as(UserDto.class).all());
|
||||
|
||||
assertThat(users).extracting(UserDto::firstname).containsExactlyInAnyOrder(firstUser.getFirstname(),
|
||||
thirdUser.getFirstname(), fourthUser.getFirstname());
|
||||
}
|
||||
|
||||
@Test // GH-2274
|
||||
void findByFluentSpecificationWithSimplePropertyPathsDoesntLoadUnrequestedPaths() {
|
||||
|
||||
@@ -2802,32 +2830,6 @@ class UserRepositoryTests {
|
||||
.containsExactlyInAnyOrder(thirdUser.getFirstname(), firstUser.getFirstname(), fourthUser.getFirstname());
|
||||
}
|
||||
|
||||
@Test // GH-2274
|
||||
void fluentSpecificationWithClassBasedDtosNotYetSupported() {
|
||||
|
||||
class UserDto {
|
||||
String firstname;
|
||||
|
||||
public UserDto() {}
|
||||
|
||||
public String getFirstname() {
|
||||
return this.firstname;
|
||||
}
|
||||
|
||||
public void setFirstname(String firstname) {
|
||||
this.firstname = firstname;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "UserDto(firstname=" + this.getFirstname() + ")";
|
||||
}
|
||||
}
|
||||
|
||||
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> {
|
||||
repository.findBy(userHasFirstnameLike("v"), q -> q.as(UserDto.class).sortBy(Sort.by("firstname")).all());
|
||||
});
|
||||
}
|
||||
|
||||
@Test // GH-2274
|
||||
void countByFluentSpecification() {
|
||||
|
||||
@@ -3457,6 +3459,10 @@ class UserRepositoryTests {
|
||||
String getFirstname();
|
||||
}
|
||||
|
||||
record UserDto(Integer id, String firstname, String lastname, String emailAddress) {
|
||||
|
||||
}
|
||||
|
||||
private interface UserProjectionUsingSpEL {
|
||||
|
||||
@Value("#{@greetingsFrom.groot(target.firstname)}")
|
||||
|
||||
@@ -20,6 +20,8 @@ import static org.assertj.core.api.Assertions.*;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.domain.Sort.Order;
|
||||
import org.springframework.data.jpa.domain.sample.User;
|
||||
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link FetchableFluentQueryByPredicate}.
|
||||
@@ -32,10 +34,13 @@ class FetchableFluentQueryByPredicateUnitTests {
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
void multipleSortBy() {
|
||||
|
||||
JpaEntityInformationSupport<User, String> entityInformation = new JpaEntityInformationSupportUnitTests.DummyJpaEntityInformation(
|
||||
User.class);
|
||||
|
||||
Sort s1 = Sort.by(Order.by("s1"));
|
||||
Sort s2 = Sort.by(Order.by("s2"));
|
||||
FetchableFluentQueryByPredicate f = new FetchableFluentQueryByPredicate(null, null, null, null, null, null, null,
|
||||
null, null);
|
||||
FetchableFluentQueryByPredicate f = new FetchableFluentQueryByPredicate(null, null, entityInformation, null, null,
|
||||
null, null, null, null, new SpelAwareProxyProjectionFactory());
|
||||
f = (FetchableFluentQueryByPredicate) f.sortBy(s1).sortBy(s2);
|
||||
assertThat(f.sort).isEqualTo(s1.and(s2));
|
||||
}
|
||||
|
||||
@@ -23,13 +23,13 @@ import jakarta.persistence.PersistenceContext;
|
||||
import java.sql.Date;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.hibernate.LazyInitializationException;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.dao.IncorrectResultSizeDataAccessException;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
@@ -403,6 +403,16 @@ class QuerydslJpaPredicateExecutorUnitTests {
|
||||
.containsExactlyInAnyOrder(dave.getFirstname(), oliver.getFirstname());
|
||||
}
|
||||
|
||||
@Test // GH-2327
|
||||
void findByFluentPredicateWithDtoProjection() {
|
||||
|
||||
List<UserProjectionDto> users = predicateExecutor.findBy(user.firstname.contains("v"),
|
||||
q -> q.as(UserProjectionDto.class).all());
|
||||
|
||||
assertThat(users).extracting(UserProjectionDto::firstname).containsExactlyInAnyOrder(dave.getFirstname(),
|
||||
oliver.getFirstname());
|
||||
}
|
||||
|
||||
@Test // GH-2294
|
||||
void findByFluentPredicateWithSortedInterfaceBasedProjection() {
|
||||
|
||||
@@ -435,31 +445,6 @@ class QuerydslJpaPredicateExecutorUnitTests {
|
||||
assertThat(exists).isTrue();
|
||||
}
|
||||
|
||||
@Test // GH-2294
|
||||
void fluentExamplesWithClassBasedDtosNotYetSupported() {
|
||||
|
||||
class UserDto {
|
||||
String firstname;
|
||||
|
||||
public UserDto() {}
|
||||
|
||||
public String getFirstname() {
|
||||
return this.firstname;
|
||||
}
|
||||
|
||||
public void setFirstname(String firstname) {
|
||||
this.firstname = firstname;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "UserDto(firstname=" + this.getFirstname() + ")";
|
||||
}
|
||||
}
|
||||
|
||||
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> predicateExecutor
|
||||
.findBy(user.firstname.contains("v"), q -> q.as(UserDto.class).sortBy(Sort.by("firstname")).all()));
|
||||
}
|
||||
|
||||
@Test // GH-2329
|
||||
void findByFluentPredicateWithSimplePropertyPathsDoesntLoadUnrequestedPaths() {
|
||||
|
||||
@@ -534,6 +519,9 @@ class QuerydslJpaPredicateExecutorUnitTests {
|
||||
|
||||
String getFirstname();
|
||||
|
||||
Set<Role> getRoles();
|
||||
String getLastname();
|
||||
}
|
||||
|
||||
public record UserProjectionDto(String firstname, String lastname) {
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user