Skip to content

Commit 2e5ed1d

Browse files
daniel-shuygregturn
authored andcommitted
Provide Specification allOf and anyOf operators to help compose Specifications..
See #1943. Original PR: #404.
1 parent 624c6c6 commit 2e5ed1d

File tree

3 files changed

+129
-16
lines changed

3 files changed

+129
-16
lines changed

spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import jakarta.persistence.criteria.Root;
2222

2323
import java.io.Serializable;
24+
import java.util.Arrays;
25+
import java.util.stream.StreamSupport;
2426

2527
import org.springframework.lang.Nullable;
2628

@@ -33,6 +35,7 @@
3335
* @author Sebastian Staudt
3436
* @author Mark Paluch
3537
* @author Jens Schauder
38+
* @author Daniel Shuy
3639
*/
3740
public interface Specification<T> extends Serializable {
3841

@@ -98,4 +101,46 @@ default Specification<T> or(@Nullable Specification<T> other) {
98101
*/
99102
@Nullable
100103
Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder);
104+
105+
/**
106+
* Applies an AND operation to all the given {@link Specification}s.
107+
*
108+
* @param specifications The {@link Specification}s to compose. Can contain {@code null}s.
109+
* @return The conjunction of the specifications
110+
* @see #and(Specification)
111+
*/
112+
static <T> Specification<T> allOf(Iterable<Specification<T>> specifications) {
113+
114+
return StreamSupport.stream(specifications.spliterator(), false) //
115+
.reduce(Specification.where(null), Specification::and);
116+
}
117+
118+
/**
119+
* @see #allOf(Iterable)
120+
*/
121+
@SafeVarargs
122+
static <T> Specification<T> allOf(Specification<T>... specifications) {
123+
return allOf(Arrays.asList(specifications));
124+
}
125+
126+
/**
127+
* Applies an OR operation to all the given {@link Specification}s.
128+
*
129+
* @param specifications The {@link Specification}s to compose. Can contain {@code null}s.
130+
* @return The disjunction of the specifications
131+
* @see #or(Specification)
132+
*/
133+
static <T> Specification<T> anyOf(Iterable<Specification<T>> specifications) {
134+
135+
return StreamSupport.stream(specifications.spliterator(), false) //
136+
.reduce(Specification.where(null), Specification::or);
137+
}
138+
139+
/**
140+
* @see #anyOf(Iterable)
141+
*/
142+
@SafeVarargs
143+
static <T> Specification<T> anyOf(Specification<T>... specifications) {
144+
return anyOf(Arrays.asList(specifications));
145+
}
101146
}

spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/SpecificationUnitTests.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
* @author Sebastian Staudt
4545
* @author Jens Schauder
4646
* @author Mark Paluch
47+
* @author Daniel Shuy
4748
*/
4849
@SuppressWarnings("serial")
4950
@ExtendWith(MockitoExtension.class)
@@ -118,6 +119,42 @@ void orConcatenatesNullSpecToSpec() {
118119
assertThat(specification.toPredicate(root, query, builder)).isEqualTo(predicate);
119120
}
120121

122+
@Test // DATAJPA-1651
123+
public void allOfConcatenatesNull() {
124+
125+
Specification<Object> specification = Specification.allOf(null, spec, null);
126+
127+
assertThat(specification).isNotNull();
128+
assertThat(specification.toPredicate(root, query, builder)).isEqualTo(predicate);
129+
}
130+
131+
@Test // DATAJPA-1651
132+
public void anyOfConcatenatesNull() {
133+
134+
Specification<Object> specification = Specification.anyOf(null, spec, null);
135+
136+
assertThat(specification).isNotNull();
137+
assertThat(specification.toPredicate(root, query, builder)).isEqualTo(predicate);
138+
}
139+
140+
@Test // DATAJPA-1651
141+
public void emptyAllOfReturnsEmptySpecification() {
142+
143+
Specification<Object> specification = Specification.allOf();
144+
145+
assertThat(specification).isNotNull();
146+
assertThat(specification.toPredicate(root, query, builder)).isNull();
147+
}
148+
149+
@Test // DATAJPA-1651
150+
public void emptyAnyOfReturnsEmptySpecification() {
151+
152+
Specification<Object> specification = Specification.anyOf();
153+
154+
assertThat(specification).isNotNull();
155+
assertThat(specification.toPredicate(root, query, builder)).isNull();
156+
}
157+
121158
@Test // DATAJPA-523
122159
void specificationsShouldBeSerializable() {
123160

spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
* @author Jesse Wouters
9595
* @author Greg Turnquist
9696
* @author Diego Krupitza
97+
* @author Daniel Shuy
9798
*/
9899
@ExtendWith(SpringExtension.class)
99100
@ContextConfiguration("classpath:application-context.xml")
@@ -489,12 +490,21 @@ void throwsExceptionForUnderSpecifiedSingleEntitySpecification() {
489490
.isThrownBy(() -> repository.findOne(userHasFirstnameLike("e")));
490491
}
491492

492-
@Test
493+
@Test // DATAJPA-1651
493494
void executesCombinedSpecificationsCorrectly() {
494495

495496
flushTestUsers();
496-
Specification<User> spec = userHasFirstname("Oliver").or(userHasLastname("Arrasz"));
497-
assertThat(repository.findAll(spec)).hasSize(2);
497+
Specification<User> spec1 = userHasFirstname("Oliver").or(userHasLastname("Arrasz"));
498+
List<User> users1 = repository.findAll(spec1);
499+
assertThat(users1).hasSize(2);
500+
501+
Specification<User> spec2 = Specification.anyOf( //
502+
userHasFirstname("Oliver"), //
503+
userHasLastname("Arrasz"));
504+
List<User> users2 = repository.findAll(spec2);
505+
assertThat(users2).hasSize(2);
506+
507+
assertThat(users1).containsExactlyInAnyOrderElementsOf(users2);
498508
}
499509

500510
@Test // DATAJPA-253
@@ -506,16 +516,27 @@ void executesNegatingSpecificationCorrectly() {
506516
assertThat(repository.findAll(spec)).containsOnly(secondUser);
507517
}
508518

509-
@Test
519+
@Test // DATAJPA-1651
510520
void executesCombinedSpecificationsWithPageableCorrectly() {
511521

512522
flushTestUsers();
513-
Specification<User> spec = userHasFirstname("Oliver").or(userHasLastname("Arrasz"));
523+
Specification<User> spec1 = userHasFirstname("Oliver").or(userHasLastname("Arrasz"));
514524

515-
Page<User> users = repository.findAll(spec, PageRequest.of(0, 1));
516-
assertThat(users.getSize()).isEqualTo(1);
517-
assertThat(users.hasPrevious()).isFalse();
518-
assertThat(users.getTotalElements()).isEqualTo(2L);
525+
Page<User> users1 = repository.findAll(spec1, PageRequest.of(0, 1));
526+
assertThat(users1.getSize()).isEqualTo(1);
527+
assertThat(users1.hasPrevious()).isFalse();
528+
assertThat(users1.getTotalElements()).isEqualTo(2L);
529+
530+
Specification<User> spec2 = Specification.anyOf( //
531+
userHasFirstname("Oliver"), //
532+
userHasLastname("Arrasz"));
533+
534+
Page<User> users2 = repository.findAll(spec2, PageRequest.of(0, 1));
535+
assertThat(users2.getSize()).isEqualTo(1);
536+
assertThat(users2.hasPrevious()).isFalse();
537+
assertThat(users2.getTotalElements()).isEqualTo(2L);
538+
539+
assertThat(users1).containsExactlyInAnyOrderElementsOf(users2);
519540
}
520541

521542
@Test
@@ -602,22 +623,22 @@ void removeDetachedObject() {
602623
assertThat(repository.count()).isEqualTo(3L);
603624
}
604625

605-
@Test
626+
@Test // DATAJPA-1651
606627
void executesPagedSpecificationsCorrectly() {
607628

608629
Page<User> result = executeSpecWithSort(Sort.unsorted());
609630
assertThat(result.getContent()).isSubsetOf(firstUser, thirdUser);
610631
}
611632

612-
@Test
633+
@Test // DATAJPA-1651
613634
void executesPagedSpecificationsWithSortCorrectly() {
614635

615636
Page<User> result = executeSpecWithSort(Sort.by(Direction.ASC, "lastname"));
616637

617638
assertThat(result.getContent()).contains(firstUser).doesNotContain(secondUser, thirdUser);
618639
}
619640

620-
@Test
641+
@Test // DATAJPA-1651
621642
void executesPagedSpecificationWithSortCorrectly2() {
622643

623644
Page<User> result = executeSpecWithSort(Sort.by(Direction.DESC, "lastname"));
@@ -2821,11 +2842,21 @@ private Page<User> executeSpecWithSort(Sort sort) {
28212842

28222843
flushTestUsers();
28232844

2824-
Specification<User> spec = userHasFirstname("Oliver").or(userHasLastname("Matthews"));
2845+
Specification<User> spec1 = userHasFirstname("Oliver").or(userHasLastname("Matthews"));
28252846

2826-
Page<User> result = repository.findAll(spec, PageRequest.of(0, 1, sort));
2827-
assertThat(result.getTotalElements()).isEqualTo(2L);
2828-
return result;
2847+
Page<User> result1 = repository.findAll(spec1, PageRequest.of(0, 1, sort));
2848+
assertThat(result1.getTotalElements()).isEqualTo(2L);
2849+
2850+
Specification<User> spec2 = Specification.anyOf( //
2851+
userHasFirstname("Oliver"), //
2852+
userHasLastname("Matthews"));
2853+
2854+
Page<User> result2 = repository.findAll(spec2, PageRequest.of(0, 1, sort));
2855+
assertThat(result2.getTotalElements()).isEqualTo(2L);
2856+
2857+
assertThat(result1).containsExactlyElementsOf(result2);
2858+
2859+
return result2;
28292860
}
28302861

28312862
private interface UserProjectionInterfaceBased {

0 commit comments

Comments
 (0)