diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java index 75708b0ec..5efadbf6b 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java @@ -21,6 +21,8 @@ import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; import java.io.Serializable; +import java.util.Arrays; +import java.util.stream.StreamSupport; import org.springframework.lang.Nullable; @@ -33,6 +35,7 @@ import org.springframework.lang.Nullable; * @author Sebastian Staudt * @author Mark Paluch * @author Jens Schauder + * @author Daniel Shuy */ public interface Specification extends Serializable { @@ -98,4 +101,46 @@ public interface Specification extends Serializable { */ @Nullable Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder criteriaBuilder); + + /** + * Applies an AND operation to all the given {@link Specification}s. + * + * @param specifications The {@link Specification}s to compose. Can contain {@code null}s. + * @return The conjunction of the specifications + * @see #and(Specification) + */ + static Specification allOf(Iterable> specifications) { + + return StreamSupport.stream(specifications.spliterator(), false) // + .reduce(Specification.where(null), Specification::and); + } + + /** + * @see #allOf(Iterable) + */ + @SafeVarargs + static Specification allOf(Specification... specifications) { + return allOf(Arrays.asList(specifications)); + } + + /** + * Applies an OR operation to all the given {@link Specification}s. + * + * @param specifications The {@link Specification}s to compose. Can contain {@code null}s. + * @return The disjunction of the specifications + * @see #or(Specification) + */ + static Specification anyOf(Iterable> specifications) { + + return StreamSupport.stream(specifications.spliterator(), false) // + .reduce(Specification.where(null), Specification::or); + } + + /** + * @see #anyOf(Iterable) + */ + @SafeVarargs + static Specification anyOf(Specification... specifications) { + return anyOf(Arrays.asList(specifications)); + } } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/SpecificationUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/SpecificationUnitTests.java index 84ca50c17..695ee67b6 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/SpecificationUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/SpecificationUnitTests.java @@ -44,6 +44,7 @@ import org.mockito.quality.Strictness; * @author Sebastian Staudt * @author Jens Schauder * @author Mark Paluch + * @author Daniel Shuy */ @SuppressWarnings("serial") @ExtendWith(MockitoExtension.class) @@ -118,6 +119,42 @@ class SpecificationUnitTests implements Serializable { assertThat(specification.toPredicate(root, query, builder)).isEqualTo(predicate); } + @Test // DATAJPA-1651 + public void allOfConcatenatesNull() { + + Specification specification = Specification.allOf(null, spec, null); + + assertThat(specification).isNotNull(); + assertThat(specification.toPredicate(root, query, builder)).isEqualTo(predicate); + } + + @Test // DATAJPA-1651 + public void anyOfConcatenatesNull() { + + Specification specification = Specification.anyOf(null, spec, null); + + assertThat(specification).isNotNull(); + assertThat(specification.toPredicate(root, query, builder)).isEqualTo(predicate); + } + + @Test // DATAJPA-1651 + public void emptyAllOfReturnsEmptySpecification() { + + Specification specification = Specification.allOf(); + + assertThat(specification).isNotNull(); + assertThat(specification.toPredicate(root, query, builder)).isNull(); + } + + @Test // DATAJPA-1651 + public void emptyAnyOfReturnsEmptySpecification() { + + Specification specification = Specification.anyOf(); + + assertThat(specification).isNotNull(); + assertThat(specification.toPredicate(root, query, builder)).isNull(); + } + @Test // DATAJPA-523 void specificationsShouldBeSerializable() { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java index fcf384fac..d120dee13 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java @@ -94,6 +94,7 @@ import org.springframework.transaction.annotation.Transactional; * @author Jesse Wouters * @author Greg Turnquist * @author Diego Krupitza + * @author Daniel Shuy */ @ExtendWith(SpringExtension.class) @ContextConfiguration("classpath:application-context.xml") @@ -489,12 +490,21 @@ public class UserRepositoryTests { .isThrownBy(() -> repository.findOne(userHasFirstnameLike("e"))); } - @Test + @Test // DATAJPA-1651 void executesCombinedSpecificationsCorrectly() { flushTestUsers(); - Specification spec = userHasFirstname("Oliver").or(userHasLastname("Arrasz")); - assertThat(repository.findAll(spec)).hasSize(2); + Specification spec1 = userHasFirstname("Oliver").or(userHasLastname("Arrasz")); + List users1 = repository.findAll(spec1); + assertThat(users1).hasSize(2); + + Specification spec2 = Specification.anyOf( // + userHasFirstname("Oliver"), // + userHasLastname("Arrasz")); + List users2 = repository.findAll(spec2); + assertThat(users2).hasSize(2); + + assertThat(users1).containsExactlyInAnyOrderElementsOf(users2); } @Test // DATAJPA-253 @@ -506,16 +516,27 @@ public class UserRepositoryTests { assertThat(repository.findAll(spec)).containsOnly(secondUser); } - @Test + @Test // DATAJPA-1651 void executesCombinedSpecificationsWithPageableCorrectly() { flushTestUsers(); - Specification spec = userHasFirstname("Oliver").or(userHasLastname("Arrasz")); + Specification spec1 = userHasFirstname("Oliver").or(userHasLastname("Arrasz")); - Page users = repository.findAll(spec, PageRequest.of(0, 1)); - assertThat(users.getSize()).isEqualTo(1); - assertThat(users.hasPrevious()).isFalse(); - assertThat(users.getTotalElements()).isEqualTo(2L); + Page users1 = repository.findAll(spec1, PageRequest.of(0, 1)); + assertThat(users1.getSize()).isEqualTo(1); + assertThat(users1.hasPrevious()).isFalse(); + assertThat(users1.getTotalElements()).isEqualTo(2L); + + Specification spec2 = Specification.anyOf( // + userHasFirstname("Oliver"), // + userHasLastname("Arrasz")); + + Page users2 = repository.findAll(spec2, PageRequest.of(0, 1)); + assertThat(users2.getSize()).isEqualTo(1); + assertThat(users2.hasPrevious()).isFalse(); + assertThat(users2.getTotalElements()).isEqualTo(2L); + + assertThat(users1).containsExactlyInAnyOrderElementsOf(users2); } @Test @@ -602,14 +623,14 @@ public class UserRepositoryTests { assertThat(repository.count()).isEqualTo(3L); } - @Test + @Test // DATAJPA-1651 void executesPagedSpecificationsCorrectly() { Page result = executeSpecWithSort(Sort.unsorted()); assertThat(result.getContent()).isSubsetOf(firstUser, thirdUser); } - @Test + @Test // DATAJPA-1651 void executesPagedSpecificationsWithSortCorrectly() { Page result = executeSpecWithSort(Sort.by(Direction.ASC, "lastname")); @@ -617,7 +638,7 @@ public class UserRepositoryTests { assertThat(result.getContent()).contains(firstUser).doesNotContain(secondUser, thirdUser); } - @Test + @Test // DATAJPA-1651 void executesPagedSpecificationWithSortCorrectly2() { Page result = executeSpecWithSort(Sort.by(Direction.DESC, "lastname")); @@ -2821,11 +2842,21 @@ public class UserRepositoryTests { flushTestUsers(); - Specification spec = userHasFirstname("Oliver").or(userHasLastname("Matthews")); + Specification spec1 = userHasFirstname("Oliver").or(userHasLastname("Matthews")); - Page result = repository.findAll(spec, PageRequest.of(0, 1, sort)); - assertThat(result.getTotalElements()).isEqualTo(2L); - return result; + Page result1 = repository.findAll(spec1, PageRequest.of(0, 1, sort)); + assertThat(result1.getTotalElements()).isEqualTo(2L); + + Specification spec2 = Specification.anyOf( // + userHasFirstname("Oliver"), // + userHasLastname("Matthews")); + + Page result2 = repository.findAll(spec2, PageRequest.of(0, 1, sort)); + assertThat(result2.getTotalElements()).isEqualTo(2L); + + assertThat(result1).containsExactlyElementsOf(result2); + + return result2; } private interface UserProjectionInterfaceBased {