diff --git a/pom.xml b/pom.xml
index dd472ffbe2..ec572e66ae 100644
--- a/pom.xml
+++ b/pom.xml
@@ -25,7 +25,7 @@
5.5.3.Final
8.0.23
42.2.19
- 2.6.0-SNAPSHOT
+ 2.6.0-2228-SNAPSHOT
0.10.3
org.hibernate
diff --git a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java
index 6c9407852e..825d8a3415 100644
--- a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java
+++ b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java
@@ -144,8 +144,8 @@ protected Predicate or(Predicate base, Predicate predicate) {
/**
* Finalizes the given {@link Predicate} and applies the given sort. Delegates to
- * {@link #complete(Predicate, Sort, CriteriaQuery, CriteriaBuilder, Root)} and hands it the current {@link CriteriaQuery}
- * and {@link CriteriaBuilder}.
+ * {@link #complete(Predicate, Sort, CriteriaQuery, CriteriaBuilder, Root)} and hands it the current
+ * {@link CriteriaQuery} and {@link CriteriaBuilder}.
*/
@Override
protected final CriteriaQuery extends Object> complete(Predicate predicate, Sort sort) {
@@ -271,10 +271,12 @@ public Predicate build() {
return getTypedPath(root, part).isNotNull();
case NOT_IN:
// cast required for eclipselink workaround, see DATAJPA-433
- return upperIfIgnoreCase(getTypedPath(root, part)).in((Expression>) provider.next(part, Collection.class).getExpression()).not();
+ return upperIfIgnoreCase(getTypedPath(root, part))
+ .in((Expression>) provider.next(part, Collection.class).getExpression()).not();
case IN:
// cast required for eclipselink workaround, see DATAJPA-433
- return upperIfIgnoreCase(getTypedPath(root, part)).in((Expression>) provider.next(part, Collection.class).getExpression());
+ return upperIfIgnoreCase(getTypedPath(root, part))
+ .in((Expression>) provider.next(part, Collection.class).getExpression());
case STARTING_WITH:
case ENDING_WITH:
case CONTAINING:
diff --git a/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryByExample.java b/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryByExample.java
new file mode 100644
index 0000000000..798f4663e5
--- /dev/null
+++ b/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryByExample.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2021 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 java.util.Collection;
+import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import javax.persistence.EntityManager;
+import javax.persistence.TypedQuery;
+
+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.mapping.PersistentEntity;
+import org.springframework.data.mapping.PersistentProperty;
+import org.springframework.data.mapping.context.MappingContext;
+import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery;
+import org.springframework.data.support.PageableExecutionUtils;
+import org.springframework.lang.Nullable;
+
+/**
+ * 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 Domain type
+ * @param Result type
+ * @author Greg Turnquist
+ * @since 2.6
+ */
+class FetchableFluentQueryByExample extends FluentQuerySupport implements FetchableFluentQuery {
+
+ private final Example example;
+ private final Function> finder;
+ private final Function, Long> countOperation;
+ private final Function, Boolean> existsOperation;
+ private final EntityManager entityManager;
+ private final EscapeCharacter escapeCharacter;
+
+ public FetchableFluentQueryByExample(Example example, Function> finder,
+ Function, Long> countOperation, Function, Boolean> existsOperation,
+ MappingContext extends PersistentEntity, ?>, ? extends PersistentProperty>> context,
+ EntityManager entityManager, EscapeCharacter escapeCharacter) {
+ this(example, (Class) example.getProbeType(), Sort.unsorted(), null, finder, countOperation, existsOperation,
+ context, entityManager, escapeCharacter);
+ }
+
+ private FetchableFluentQueryByExample(Example example, Class returnType, Sort sort,
+ @Nullable Collection properties, Function> finder,
+ Function, Long> countOperation, Function, Boolean> existsOperation,
+ MappingContext extends PersistentEntity, ?>, ? extends PersistentProperty>> context,
+ EntityManager entityManager, EscapeCharacter escapeCharacter) {
+
+ super(returnType, sort, properties, context);
+ this.example = example;
+ this.finder = finder;
+ this.countOperation = countOperation;
+ this.existsOperation = existsOperation;
+ this.entityManager = entityManager;
+ this.escapeCharacter = escapeCharacter;
+ }
+
+ @Override
+ public FetchableFluentQuery sortBy(Sort sort) {
+
+ return new FetchableFluentQueryByExample(this.example, this.resultType, this.sort.and(sort), this.properties,
+ this.finder, this.countOperation, this.existsOperation, this.context, this.entityManager, this.escapeCharacter);
+ }
+
+ @Override
+ public FetchableFluentQuery as(Class resultType) {
+
+ if (!resultType.isInterface()) {
+ throw new UnsupportedOperationException("Class-based DTOs are not yet supported.");
+ }
+
+ return new FetchableFluentQueryByExample(this.example, resultType, this.sort, this.properties, this.finder,
+ this.countOperation, this.existsOperation, this.context, this.entityManager, this.escapeCharacter);
+ }
+
+ @Override
+ public FetchableFluentQuery project(Collection properties) {
+
+ return new FetchableFluentQueryByExample<>(this.example, this.resultType, this.sort, mergeProperties(properties),
+ this.finder, this.countOperation, this.existsOperation, this.context, this.entityManager, this.escapeCharacter);
+ }
+
+ @Override
+ public R oneValue() {
+
+ TypedQuery limitedQuery = this.finder.apply(this.sort);
+ limitedQuery.setMaxResults(2); // Never need more than 2 values
+
+ List results = limitedQuery //
+ .getResultStream() //
+ .map(getConversionFunction(this.example.getProbeType(), this.resultType)) //
+ .collect(Collectors.toList());
+ ;
+
+ if (results.size() > 1) {
+ throw new IncorrectResultSizeDataAccessException(1);
+ }
+
+ return results.isEmpty() ? null : results.get(0);
+ }
+
+ @Override
+ public R firstValue() {
+
+ TypedQuery limitedQuery = this.finder.apply(this.sort);
+ limitedQuery.setMaxResults(1); // Never need more than 1 value
+
+ List results = limitedQuery //
+ .getResultStream() //
+ .map(getConversionFunction(this.example.getProbeType(), this.resultType)) //
+ .collect(Collectors.toList());
+
+ return results.isEmpty() ? null : results.get(0);
+ }
+
+ @Override
+ public List all() {
+ return stream().collect(Collectors.toList());
+ }
+
+ @Override
+ public Page page(Pageable pageable) {
+ return pageable.isUnpaged() ? new PageImpl<>(all()) : readPage(pageable);
+ }
+
+ @Override
+ public Stream stream() {
+
+ return this.finder.apply(this.sort) //
+ .getResultStream() //
+ .map(getConversionFunction(this.example.getProbeType(), this.resultType));
+ }
+
+ @Override
+ public long count() {
+ return this.countOperation.apply(example);
+ }
+
+ @Override
+ public boolean exists() {
+ return this.existsOperation.apply(example);
+ }
+
+ private Page readPage(Pageable pageable) {
+
+ TypedQuery pagedQuery = this.finder.apply(this.sort);
+
+ if (pageable.isPaged()) {
+ pagedQuery.setFirstResult((int) pageable.getOffset());
+ pagedQuery.setMaxResults(pageable.getPageSize());
+ }
+
+ List paginatedResults = pagedQuery.getResultStream() //
+ .map(getConversionFunction(this.example.getProbeType(), this.resultType)) //
+ .collect(Collectors.toList());
+
+ return PageableExecutionUtils.getPage(paginatedResults, pageable, () -> this.countOperation.apply(this.example));
+ }
+}
diff --git a/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryByPredicate.java b/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryByPredicate.java
new file mode 100644
index 0000000000..ba2b0790fc
--- /dev/null
+++ b/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryByPredicate.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2021 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 java.util.Collection;
+import java.util.List;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.springframework.dao.IncorrectResultSizeDataAccessException;
+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.mapping.PersistentEntity;
+import org.springframework.data.mapping.PersistentProperty;
+import org.springframework.data.mapping.context.MappingContext;
+import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery;
+import org.springframework.data.support.PageableExecutionUtils;
+import org.springframework.lang.Nullable;
+
+import com.querydsl.core.types.Predicate;
+import com.querydsl.jpa.JPQLQuery;
+
+/**
+ * Immutable implementation of {@link FetchableFluentQuery} based on a Querydsl {@link Predicate}. All methods that
+ * return a {@link FetchableFluentQuery} will return a new instance, not the original.
+ *
+ * @param Domain type
+ * @param Result type
+ * @author Greg Turnquist
+ * @since 2.6
+ */
+class FetchableFluentQueryByPredicate extends FluentQuerySupport implements FetchableFluentQuery {
+
+ private final Predicate predicate;
+ private final Function> finder;
+ private final BiFunction> pagedFinder;
+ private final Function countOperation;
+ private final Function existsOperation;
+ private final Class entityType;
+
+ public FetchableFluentQueryByPredicate(Predicate predicate, Class resultType, Function> finder,
+ BiFunction> pagedFinder, Function countOperation,
+ Function existsOperation, Class entityType,
+ MappingContext extends PersistentEntity, ?>, ? extends PersistentProperty>> context) {
+ this(predicate, resultType, Sort.unsorted(), null, finder, pagedFinder, countOperation, existsOperation, entityType,
+ context);
+ }
+
+ private FetchableFluentQueryByPredicate(Predicate predicate, Class resultType, Sort sort,
+ @Nullable Collection properties, Function> finder,
+ BiFunction> pagedFinder, Function countOperation,
+ Function existsOperation, Class entityType,
+ MappingContext extends PersistentEntity, ?>, ? extends PersistentProperty>> context) {
+
+ super(resultType, sort, properties, context);
+ this.predicate = predicate;
+ this.finder = finder;
+ this.pagedFinder = pagedFinder;
+ this.countOperation = countOperation;
+ this.existsOperation = existsOperation;
+ this.entityType = entityType;
+ }
+
+ @Override
+ public FetchableFluentQuery sortBy(Sort sort) {
+
+ return new FetchableFluentQueryByPredicate<>(this.predicate, this.resultType, this.sort.and(sort), this.properties,
+ this.finder, this.pagedFinder, this.countOperation, this.existsOperation, this.entityType, this.context);
+ }
+
+ @Override
+ public FetchableFluentQuery as(Class resultType) {
+
+ if (!resultType.isInterface()) {
+ throw new UnsupportedOperationException("Class-based DTOs are not yet supported.");
+ }
+
+ return new FetchableFluentQueryByPredicate<>(this.predicate, resultType, this.sort, this.properties, this.finder,
+ this.pagedFinder, this.countOperation, this.existsOperation, this.entityType, this.context);
+ }
+
+ @Override
+ public FetchableFluentQuery project(Collection properties) {
+
+ return new FetchableFluentQueryByPredicate<>(this.predicate, this.resultType, this.sort,
+ mergeProperties(properties), this.finder, this.pagedFinder, this.countOperation, this.existsOperation,
+ this.entityType, this.context);
+ }
+
+ @Override
+ public R oneValue() {
+
+ List results = this.finder.apply(this.sort) //
+ .limit(2) // Never need more than 2 values
+ .stream() //
+ .map(getConversionFunction(this.entityType, this.resultType)) //
+ .collect(Collectors.toList());
+
+ if (results.size() > 1) {
+ throw new IncorrectResultSizeDataAccessException(1);
+ }
+
+ return results.isEmpty() ? null : results.get(0);
+ }
+
+ @Override
+ public R firstValue() {
+
+ List results = this.finder.apply(this.sort) //
+ .limit(1) // Never need more than 1 value
+ .stream() //
+ .map(getConversionFunction(this.entityType, this.resultType)) //
+ .collect(Collectors.toList());
+
+ return results.isEmpty() ? null : results.get(0);
+ }
+
+ @Override
+ public List all() {
+ return stream().collect(Collectors.toList());
+ }
+
+ @Override
+ public Page page(Pageable pageable) {
+ return pageable.isUnpaged() ? new PageImpl<>(all()) : readPage(pageable);
+ }
+
+ @Override
+ public Stream stream() {
+
+ return this.finder.apply(this.sort) //
+ .stream() //
+ .map(getConversionFunction(this.entityType, this.resultType));
+ }
+
+ @Override
+ public long count() {
+ return this.countOperation.apply(this.predicate);
+ }
+
+ @Override
+ public boolean exists() {
+ return this.existsOperation.apply(this.predicate);
+ }
+
+ private Page readPage(Pageable pageable) {
+
+ JPQLQuery pagedQuery = this.pagedFinder.apply(this.sort, pageable);
+
+ List paginatedResults = pagedQuery.stream() //
+ .map(getConversionFunction(this.entityType, this.resultType)) //
+ .collect(Collectors.toList());
+
+ return PageableExecutionUtils.getPage(paginatedResults, pageable, () -> this.countOperation.apply(this.predicate));
+ }
+}
diff --git a/src/main/java/org/springframework/data/jpa/repository/support/FluentQuerySupport.java b/src/main/java/org/springframework/data/jpa/repository/support/FluentQuerySupport.java
new file mode 100644
index 0000000000..d8cf794577
--- /dev/null
+++ b/src/main/java/org/springframework/data/jpa/repository/support/FluentQuerySupport.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2021 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 java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.function.Function;
+
+import org.springframework.core.convert.support.DefaultConversionService;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.mapping.PersistentEntity;
+import org.springframework.data.mapping.PersistentProperty;
+import org.springframework.data.mapping.context.MappingContext;
+import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
+import org.springframework.lang.Nullable;
+
+/**
+ * Supporting class containing some state and convenience methods for building and executing fluent queries.
+ *
+ * @param The resulting type of the query.
+ * @author Greg Turnquist
+ * @since 2.6
+ */
+abstract class FluentQuerySupport {
+
+ protected final Class resultType;
+ protected final Sort sort;
+ protected final @Nullable Set properties;
+ protected final MappingContext extends PersistentEntity, ?>, ? extends PersistentProperty>> context;
+
+ private final SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory();
+
+ FluentQuerySupport(Class resultType, Sort sort, @Nullable Collection properties,
+ MappingContext extends PersistentEntity, ?>, ? extends PersistentProperty>> context) {
+
+ this.resultType = resultType;
+ this.sort = sort;
+
+ if (properties != null) {
+ this.properties = new HashSet<>(properties);
+ } else {
+ this.properties = null;
+ }
+
+ this.context = context;
+ }
+
+ final Collection mergeProperties(Collection additionalProperties) {
+
+ Set newProperties = new HashSet<>();
+ if (this.properties != null) {
+ newProperties.addAll(this.properties);
+ }
+ newProperties.addAll(additionalProperties);
+ return Collections.unmodifiableCollection(newProperties);
+ }
+
+ @SuppressWarnings("unchecked")
+ final Function