Add fluent findBy API to JpaSpecificationExecutor.
Extend fluent findBy support through the usage of Specifications. See #2274.
This commit is contained in:
@@ -15,13 +15,13 @@
|
||||
*/
|
||||
package org.springframework.data.jpa.domain;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import jakarta.persistence.criteria.CriteriaBuilder;
|
||||
import jakarta.persistence.criteria.CriteriaQuery;
|
||||
import jakarta.persistence.criteria.Predicate;
|
||||
import jakarta.persistence.criteria.Root;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,11 +17,13 @@ package org.springframework.data.jpa.repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.data.repository.query.FluentQuery;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
@@ -84,4 +86,16 @@ public interface JpaSpecificationExecutor<T> {
|
||||
* <code>false</code>.
|
||||
*/
|
||||
boolean exists(Specification<T> spec);
|
||||
|
||||
/**
|
||||
* Returns entities matching the given {@link Specification} applying the {@code queryFunction} that defines the query
|
||||
* and its result type.
|
||||
*
|
||||
* @param spec – must not be null.
|
||||
* @param queryFunction – the query function defining projection, sorting, and the result type
|
||||
* @return all entities matching the given Example.
|
||||
* @since 3.0
|
||||
*/
|
||||
<S extends T, R> R findBy(Specification<T> spec, Function<FluentQuery.FetchableFluentQuery<S>, R> queryFunction);
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
/*
|
||||
* Copyright 2021-2022 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.Page;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.data.repository.query.FluentQuery;
|
||||
import org.springframework.data.support.PageableExecutionUtils;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Immutable implementation of {@link FetchableFluentQuery} based on a {@link Specification}. 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
|
||||
* @since 3.0
|
||||
*/
|
||||
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 Function<Specification<S>, Long> countOperation;
|
||||
private final Function<Specification<S>, Boolean> existsOperation;
|
||||
private final EntityManager entityManager;
|
||||
|
||||
public FetchableFluentQueryBySpecification(Specification<S> spec, Class<S> entityType, Sort sort,
|
||||
Collection<String> properties, Function<Sort, TypedQuery<S>> finder,
|
||||
Function<Specification<S>, Long> countOperation, Function<Specification<S>, Boolean> existsOperation,
|
||||
EntityManager entityManager) {
|
||||
this(spec, entityType, (Class<R>) entityType, Sort.unsorted(), Collections.emptySet(), finder, countOperation,
|
||||
existsOperation, entityManager);
|
||||
}
|
||||
|
||||
private FetchableFluentQueryBySpecification(Specification<S> spec, Class<S> entityType, Class<R> resultType,
|
||||
Sort sort, Collection<String> properties, Function<Sort, TypedQuery<S>> finder,
|
||||
Function<Specification<S>, Long> countOperation, Function<Specification<S>, Boolean> existsOperation,
|
||||
EntityManager entityManager) {
|
||||
|
||||
super(resultType, sort, properties, entityType);
|
||||
this.spec = spec;
|
||||
this.finder = finder;
|
||||
this.countOperation = countOperation;
|
||||
this.existsOperation = existsOperation;
|
||||
this.entityManager = entityManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FetchableFluentQuery<R> sortBy(Sort sort) {
|
||||
|
||||
Assert.notNull(sort, "Sort must not be null!");
|
||||
|
||||
return new FetchableFluentQueryBySpecification<>(spec, entityType, resultType, this.sort.and(sort), properties,
|
||||
finder, countOperation, existsOperation, entityManager);
|
||||
}
|
||||
|
||||
@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 FetchableFluentQueryBySpecification<>(spec, entityType, resultType, sort, properties, finder,
|
||||
countOperation, existsOperation, entityManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FetchableFluentQuery<R> project(Collection<String> properties) {
|
||||
|
||||
return new FetchableFluentQueryBySpecification<>(spec, entityType, resultType, sort, properties, finder,
|
||||
countOperation, existsOperation, entityManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public R oneValue() {
|
||||
|
||||
List<?> results = createSortedAndProjectedQuery() //
|
||||
.setMaxResults(2) // Never need more than 2 values
|
||||
.getResultList();
|
||||
|
||||
if (results.size() > 1) {
|
||||
throw new IncorrectResultSizeDataAccessException(1);
|
||||
}
|
||||
|
||||
return results.isEmpty() ? null : getConversionFunction().apply(results.get(0));
|
||||
}
|
||||
|
||||
@Override
|
||||
public R firstValue() {
|
||||
|
||||
List<?> results = createSortedAndProjectedQuery() //
|
||||
.setMaxResults(1) // Never need more than 1 value
|
||||
.getResultList();
|
||||
|
||||
return results.isEmpty() ? null : getConversionFunction().apply(results.get(0));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<R> all() {
|
||||
return convert(createSortedAndProjectedQuery().getResultList());
|
||||
}
|
||||
|
||||
@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(spec);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists() {
|
||||
return existsOperation.apply(spec);
|
||||
}
|
||||
|
||||
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 Page<R> readPage(Pageable pageable) {
|
||||
|
||||
TypedQuery<S> pagedQuery = createSortedAndProjectedQuery();
|
||||
|
||||
if (pageable.isPaged()) {
|
||||
pagedQuery.setFirstResult((int) pageable.getOffset());
|
||||
pagedQuery.setMaxResults(pageable.getPageSize());
|
||||
}
|
||||
|
||||
List<R> paginatedResults = convert(pagedQuery.getResultList());
|
||||
|
||||
return PageableExecutionUtils.getPage(paginatedResults, pageable, () -> countOperation.apply(spec));
|
||||
}
|
||||
|
||||
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(entityType, resultType);
|
||||
}
|
||||
}
|
||||
@@ -17,15 +17,6 @@ package org.springframework.data.jpa.repository.support;
|
||||
|
||||
import static org.springframework.data.jpa.repository.query.QueryUtils.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.LockModeType;
|
||||
import jakarta.persistence.NoResultException;
|
||||
@@ -39,6 +30,15 @@ import jakarta.persistence.criteria.Path;
|
||||
import jakarta.persistence.criteria.Predicate;
|
||||
import jakarta.persistence.criteria.Root;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.dao.EmptyResultDataAccessException;
|
||||
import org.springframework.data.domain.Example;
|
||||
import org.springframework.data.domain.Page;
|
||||
@@ -515,6 +515,22 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
|
||||
return queryFunction.apply(fluentQuery);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S extends T, R> R findBy(Specification<T> spec, Function<FetchableFluentQuery<S>, R> queryFunction) {
|
||||
|
||||
Assert.notNull(spec, "Specification must not be null!");
|
||||
Assert.notNull(queryFunction, "Query function must not be null!");
|
||||
|
||||
Function<Sort, TypedQuery<T>> finder = sort -> {
|
||||
return getQuery(spec, getDomainClass(), sort);
|
||||
};
|
||||
|
||||
FetchableFluentQuery<R> fluentQuery = new FetchableFluentQueryBySpecification<T, R>(spec, getDomainClass(),
|
||||
Sort.unsorted(), null, finder, this::count, this::exists, this.em);
|
||||
|
||||
return queryFunction.apply((FetchableFluentQuery<S>) fluentQuery);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long count() {
|
||||
return em.createQuery(getCountQueryString(), Long.class).getSingleResult();
|
||||
|
||||
@@ -24,6 +24,13 @@ import static org.springframework.data.jpa.domain.Specification.*;
|
||||
import static org.springframework.data.jpa.domain.Specification.not;
|
||||
import static org.springframework.data.jpa.domain.sample.UserSpecifications.*;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.PersistenceContext;
|
||||
import jakarta.persistence.Query;
|
||||
import jakarta.persistence.criteria.CriteriaBuilder;
|
||||
import jakarta.persistence.criteria.CriteriaQuery;
|
||||
import jakarta.persistence.criteria.Predicate;
|
||||
import jakarta.persistence.criteria.Root;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -36,14 +43,6 @@ import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.PersistenceContext;
|
||||
import jakarta.persistence.Query;
|
||||
import jakarta.persistence.criteria.CriteriaBuilder;
|
||||
import jakarta.persistence.criteria.CriteriaQuery;
|
||||
import jakarta.persistence.criteria.Predicate;
|
||||
import jakarta.persistence.criteria.Root;
|
||||
|
||||
import org.assertj.core.api.SoftAssertions;
|
||||
import org.hibernate.LazyInitializationException;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -2298,6 +2297,175 @@ public class UserRepositoryTests {
|
||||
assertThat(exists).isTrue();
|
||||
}
|
||||
|
||||
@Test // GH-2274
|
||||
void findByFluentSpecificationWithSorting() {
|
||||
|
||||
flushTestUsers();
|
||||
|
||||
List<User> users = repository.findBy(userHasFirstnameLike("v"), q -> q.sortBy(Sort.by("firstname")).all());
|
||||
|
||||
assertThat(users).containsExactly(thirdUser, firstUser, fourthUser);
|
||||
}
|
||||
|
||||
@Test // GH-2274
|
||||
void findByFluentSpecificationFirstValue() {
|
||||
|
||||
flushTestUsers();
|
||||
|
||||
User firstUser = repository.findBy(userHasFirstnameLike("v"), q -> q.sortBy(Sort.by("firstname")).firstValue());
|
||||
|
||||
assertThat(firstUser).isEqualTo(thirdUser);
|
||||
}
|
||||
|
||||
@Test // GH-2274
|
||||
void findByFluentSpecificationOneValue() {
|
||||
|
||||
flushTestUsers();
|
||||
|
||||
assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class)
|
||||
.isThrownBy(() -> repository.findBy(userHasFirstnameLike("v"), q -> q.sortBy(Sort.by("firstname")).oneValue()));
|
||||
}
|
||||
|
||||
@Test // GH-2274
|
||||
void findByFluentSpecificationStream() {
|
||||
|
||||
flushTestUsers();
|
||||
|
||||
Stream<User> userStream = repository.findBy(userHasFirstnameLike("v"),
|
||||
q -> q.sortBy(Sort.by("firstname")).stream());
|
||||
|
||||
assertThat(userStream).containsExactly(thirdUser, firstUser, fourthUser);
|
||||
}
|
||||
|
||||
@Test // GH-2274
|
||||
void findByFluentSpecificationPage() {
|
||||
|
||||
flushTestUsers();
|
||||
|
||||
Page<User> page0 = repository.findBy(userHasFirstnameLike("v"),
|
||||
q -> q.sortBy(Sort.by("firstname")).page(PageRequest.of(0, 2)));
|
||||
|
||||
Page<User> page1 = repository.findBy(userHasFirstnameLike("v"),
|
||||
q -> q.sortBy(Sort.by("firstname")).page(PageRequest.of(1, 2)));
|
||||
|
||||
assertThat(page0.getContent()).containsExactly(thirdUser, firstUser);
|
||||
assertThat(page1.getContent()).containsExactly(fourthUser);
|
||||
}
|
||||
|
||||
@Test // GH-2274
|
||||
void findByFluentSpecificationWithInterfaceBasedProjection() {
|
||||
|
||||
flushTestUsers();
|
||||
|
||||
List<UserProjectionInterfaceBased> users = repository.findBy(userHasFirstnameLike("v"),
|
||||
q -> q.as(UserProjectionInterfaceBased.class).all());
|
||||
|
||||
assertThat(users).extracting(UserProjectionInterfaceBased::getFirstname)
|
||||
.containsExactlyInAnyOrder(firstUser.getFirstname(), thirdUser.getFirstname(), fourthUser.getFirstname());
|
||||
}
|
||||
|
||||
@Test // GH-2274
|
||||
void findByFluentSpecificationWithSimplePropertyPathsDoesntLoadUnrequestedPaths() {
|
||||
|
||||
flushTestUsers();
|
||||
// make sure we don't get preinitialized entities back:
|
||||
em.clear();
|
||||
|
||||
List<User> users = repository.findBy(userHasFirstnameLike("v"), q -> q.project("firstname").all());
|
||||
|
||||
// remove the entities, so lazy loading throws an exception
|
||||
em.clear();
|
||||
|
||||
assertThat(users).extracting(User::getFirstname).containsExactlyInAnyOrder(firstUser.getFirstname(),
|
||||
thirdUser.getFirstname(), fourthUser.getFirstname());
|
||||
|
||||
assertThatExceptionOfType(LazyInitializationException.class) //
|
||||
.isThrownBy( //
|
||||
() -> users.forEach(u -> u.getRoles().size()) // forces loading of roles
|
||||
);
|
||||
}
|
||||
|
||||
@Test // GH-2274
|
||||
void findByFluentSpecificationWithCollectionPropertyPathsDoesntLoadUnrequestedPaths() {
|
||||
|
||||
flushTestUsers();
|
||||
// make sure we don't get preinitialized entities back:
|
||||
em.clear();
|
||||
|
||||
List<User> users = repository.findBy(userHasFirstnameLike("v"), q -> q.project("firstname", "roles").all());
|
||||
|
||||
// remove the entities, so lazy loading throws an exception
|
||||
em.clear();
|
||||
|
||||
assertThat(users).extracting(User::getFirstname).containsExactlyInAnyOrder(firstUser.getFirstname(),
|
||||
thirdUser.getFirstname(), fourthUser.getFirstname());
|
||||
|
||||
assertThat(users).allMatch(u -> u.getRoles().isEmpty());
|
||||
}
|
||||
|
||||
@Test // GH-2274
|
||||
void findByFluentSpecificationWithComplexPropertyPathsDoesntLoadUnrequestedPaths() {
|
||||
|
||||
flushTestUsers();
|
||||
// make sure we don't get preinitialized entities back:
|
||||
em.clear();
|
||||
|
||||
List<User> users = repository.findBy(userHasFirstnameLike("v"), q -> q.project("roles.name").all());
|
||||
|
||||
// remove the entities, so lazy loading throws an exception
|
||||
em.clear();
|
||||
|
||||
assertThat(users).extracting(User::getFirstname).containsExactlyInAnyOrder(firstUser.getFirstname(),
|
||||
thirdUser.getFirstname(), fourthUser.getFirstname());
|
||||
|
||||
assertThat(users).allMatch(u -> u.getRoles().isEmpty());
|
||||
}
|
||||
|
||||
@Test // GH-2274
|
||||
void findByFluentSpecificationWithSortedInterfaceBasedProjection() {
|
||||
|
||||
flushTestUsers();
|
||||
|
||||
List<UserProjectionInterfaceBased> users = repository.findBy(userHasFirstnameLike("v"),
|
||||
q -> q.as(UserProjectionInterfaceBased.class).sortBy(Sort.by("firstname")).all());
|
||||
|
||||
assertThat(users).extracting(UserProjectionInterfaceBased::getFirstname)
|
||||
.containsExactlyInAnyOrder(thirdUser.getFirstname(), firstUser.getFirstname(), fourthUser.getFirstname());
|
||||
}
|
||||
|
||||
@Test // GH-2274
|
||||
void fluentSpecificationWithClassBasedDtosNotYetSupported() {
|
||||
|
||||
@Data
|
||||
class UserDto {
|
||||
String firstname;
|
||||
}
|
||||
|
||||
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> {
|
||||
repository.findBy(userHasFirstnameLike("v"), q -> q.as(UserDto.class).sortBy(Sort.by("firstname")).all());
|
||||
});
|
||||
}
|
||||
|
||||
@Test // GH-2274
|
||||
void countByFluentSpecification() {
|
||||
|
||||
flushTestUsers();
|
||||
|
||||
long numOfUsers = repository.findBy(userHasFirstnameLike("v"), q -> q.sortBy(Sort.by("firstname")).count());
|
||||
|
||||
assertThat(numOfUsers).isEqualTo(3);
|
||||
}
|
||||
|
||||
@Test // GH-2274
|
||||
void existsByFluentSpecification() {
|
||||
|
||||
flushTestUsers();
|
||||
|
||||
boolean exists = repository.findBy(userHasFirstnameLike("v"), q -> q.sortBy(Sort.by("firstname")).exists());
|
||||
|
||||
assertThat(exists).isTrue();
|
||||
}
|
||||
|
||||
@Test // DATAJPA-218
|
||||
void countByExampleWithExcludedAttributes() {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user