Provide Specification allOf and anyOf operators to help compose Specifications..

See #1943.

Original PR: #404.
This commit is contained in:
Daniel Shuy
2019-12-28 23:19:44 +08:00
committed by Greg L. Turnquist
parent 624c6c681f
commit 2e5ed1d0ec
3 changed files with 129 additions and 16 deletions

View File

@@ -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<T> extends Serializable {
@@ -98,4 +101,46 @@ public interface Specification<T> extends Serializable {
*/
@Nullable
Predicate toPredicate(Root<T> 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 <T> Specification<T> allOf(Iterable<Specification<T>> specifications) {
return StreamSupport.stream(specifications.spliterator(), false) //
.reduce(Specification.where(null), Specification::and);
}
/**
* @see #allOf(Iterable)
*/
@SafeVarargs
static <T> Specification<T> allOf(Specification<T>... 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 <T> Specification<T> anyOf(Iterable<Specification<T>> specifications) {
return StreamSupport.stream(specifications.spliterator(), false) //
.reduce(Specification.where(null), Specification::or);
}
/**
* @see #anyOf(Iterable)
*/
@SafeVarargs
static <T> Specification<T> anyOf(Specification<T>... specifications) {
return anyOf(Arrays.asList(specifications));
}
}

View File

@@ -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<Object> specification = Specification.allOf(null, spec, null);
assertThat(specification).isNotNull();
assertThat(specification.toPredicate(root, query, builder)).isEqualTo(predicate);
}
@Test // DATAJPA-1651
public void anyOfConcatenatesNull() {
Specification<Object> specification = Specification.anyOf(null, spec, null);
assertThat(specification).isNotNull();
assertThat(specification.toPredicate(root, query, builder)).isEqualTo(predicate);
}
@Test // DATAJPA-1651
public void emptyAllOfReturnsEmptySpecification() {
Specification<Object> specification = Specification.allOf();
assertThat(specification).isNotNull();
assertThat(specification.toPredicate(root, query, builder)).isNull();
}
@Test // DATAJPA-1651
public void emptyAnyOfReturnsEmptySpecification() {
Specification<Object> specification = Specification.anyOf();
assertThat(specification).isNotNull();
assertThat(specification.toPredicate(root, query, builder)).isNull();
}
@Test // DATAJPA-523
void specificationsShouldBeSerializable() {

View File

@@ -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<User> spec = userHasFirstname("Oliver").or(userHasLastname("Arrasz"));
assertThat(repository.findAll(spec)).hasSize(2);
Specification<User> spec1 = userHasFirstname("Oliver").or(userHasLastname("Arrasz"));
List<User> users1 = repository.findAll(spec1);
assertThat(users1).hasSize(2);
Specification<User> spec2 = Specification.anyOf( //
userHasFirstname("Oliver"), //
userHasLastname("Arrasz"));
List<User> 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<User> spec = userHasFirstname("Oliver").or(userHasLastname("Arrasz"));
Specification<User> spec1 = userHasFirstname("Oliver").or(userHasLastname("Arrasz"));
Page<User> users = repository.findAll(spec, PageRequest.of(0, 1));
assertThat(users.getSize()).isEqualTo(1);
assertThat(users.hasPrevious()).isFalse();
assertThat(users.getTotalElements()).isEqualTo(2L);
Page<User> users1 = repository.findAll(spec1, PageRequest.of(0, 1));
assertThat(users1.getSize()).isEqualTo(1);
assertThat(users1.hasPrevious()).isFalse();
assertThat(users1.getTotalElements()).isEqualTo(2L);
Specification<User> spec2 = Specification.anyOf( //
userHasFirstname("Oliver"), //
userHasLastname("Arrasz"));
Page<User> 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<User> result = executeSpecWithSort(Sort.unsorted());
assertThat(result.getContent()).isSubsetOf(firstUser, thirdUser);
}
@Test
@Test // DATAJPA-1651
void executesPagedSpecificationsWithSortCorrectly() {
Page<User> 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<User> result = executeSpecWithSort(Sort.by(Direction.DESC, "lastname"));
@@ -2821,11 +2842,21 @@ public class UserRepositoryTests {
flushTestUsers();
Specification<User> spec = userHasFirstname("Oliver").or(userHasLastname("Matthews"));
Specification<User> spec1 = userHasFirstname("Oliver").or(userHasLastname("Matthews"));
Page<User> result = repository.findAll(spec, PageRequest.of(0, 1, sort));
assertThat(result.getTotalElements()).isEqualTo(2L);
return result;
Page<User> result1 = repository.findAll(spec1, PageRequest.of(0, 1, sort));
assertThat(result1.getTotalElements()).isEqualTo(2L);
Specification<User> spec2 = Specification.anyOf( //
userHasFirstname("Oliver"), //
userHasLastname("Matthews"));
Page<User> result2 = repository.findAll(spec2, PageRequest.of(0, 1, sort));
assertThat(result2.getTotalElements()).isEqualTo(2L);
assertThat(result1).containsExactlyElementsOf(result2);
return result2;
}
private interface UserProjectionInterfaceBased {