Skip to content

Commit 578f539

Browse files
mp911dechristophstrobl
authored andcommitted
Explore refined Specification API.
Introduce DeleteSpecification and UpdateSpecification. Add PredicateSpecification. Update SpecificationExecutor. Closes: #3521 Original Pull Request: #3578
1 parent 84706b5 commit 578f539

14 files changed

+1687
-221
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jpa.domain;
17+
18+
import jakarta.persistence.criteria.CriteriaBuilder;
19+
import jakarta.persistence.criteria.CriteriaDelete;
20+
import jakarta.persistence.criteria.Predicate;
21+
import jakarta.persistence.criteria.Root;
22+
23+
import java.io.Serial;
24+
import java.io.Serializable;
25+
import java.util.Arrays;
26+
import java.util.stream.StreamSupport;
27+
28+
import org.springframework.lang.Nullable;
29+
import org.springframework.util.Assert;
30+
31+
/**
32+
* Specification in the sense of Domain Driven Design to handle Criteria Deletes.
33+
*
34+
* @author Mark Paluch
35+
* @since xxx
36+
*/
37+
@FunctionalInterface
38+
public interface DeleteSpecification<T> extends Serializable {
39+
40+
@Serial long serialVersionUID = 1L;
41+
42+
/**
43+
* Simple static factory method to create a specification deleting all objects.
44+
*
45+
* @param <T> the type of the {@link Root} the resulting {@literal DeleteSpecification} operates on.
46+
* @return guaranteed to be not {@literal null}.
47+
*/
48+
static <T> DeleteSpecification<T> all() {
49+
return (root, query, builder) -> null;
50+
}
51+
52+
/**
53+
* Simple static factory method to add some syntactic sugar around a {@literal DeleteSpecification}.
54+
*
55+
* @param <T> the type of the {@link Root} the resulting {@literal DeleteSpecification} operates on.
56+
* @param spec must not be {@literal null}.
57+
* @return guaranteed to be not {@literal null}.
58+
*/
59+
static <T> DeleteSpecification<T> where(DeleteSpecification<T> spec) {
60+
61+
Assert.notNull(spec, "DeleteSpecification must not be null");
62+
63+
return spec;
64+
}
65+
66+
/**
67+
* Simple static factory method to add some syntactic sugar translating {@link PredicateSpecification} to
68+
* {@link DeleteSpecification}.
69+
*
70+
* @param <T> the type of the {@link Root} the resulting {@literal DeleteSpecification} operates on.
71+
* @param spec the {@link PredicateSpecification} to wrap.
72+
* @return guaranteed to be not {@literal null}.
73+
*/
74+
static <T> DeleteSpecification<T> where(PredicateSpecification<T> spec) {
75+
76+
Assert.notNull(spec, "PredicateSpecification must not be null");
77+
78+
return where((root, delete, criteriaBuilder) -> spec.toPredicate(root, criteriaBuilder));
79+
}
80+
81+
/**
82+
* ANDs the given {@link DeleteSpecification} to the current one.
83+
*
84+
* @param other the other {@link DeleteSpecification}.
85+
* @return the conjunction of the specifications.
86+
*/
87+
default DeleteSpecification<T> and(DeleteSpecification<T> other) {
88+
89+
Assert.notNull(other, "Other specification must not be null");
90+
91+
return SpecificationComposition.composed(this, other, CriteriaBuilder::and);
92+
}
93+
94+
/**
95+
* ANDs the given {@link DeleteSpecification} to the current one.
96+
*
97+
* @param other the other {@link PredicateSpecification}.
98+
* @return the conjunction of the specifications.
99+
*/
100+
default DeleteSpecification<T> and(PredicateSpecification<T> other) {
101+
102+
Assert.notNull(other, "Other specification must not be null");
103+
104+
return SpecificationComposition.composed(this, where(other), CriteriaBuilder::and);
105+
}
106+
107+
/**
108+
* ORs the given specification to the current one.
109+
*
110+
* @param other the other {@link DeleteSpecification}.
111+
* @return the disjunction of the specifications.
112+
*/
113+
default DeleteSpecification<T> or(DeleteSpecification<T> other) {
114+
115+
Assert.notNull(other, "Other specification must not be null");
116+
117+
return SpecificationComposition.composed(this, other, CriteriaBuilder::or);
118+
}
119+
120+
/**
121+
* ORs the given specification to the current one.
122+
*
123+
* @param other the other {@link PredicateSpecification}.
124+
* @return the disjunction of the specifications.
125+
*/
126+
default DeleteSpecification<T> or(PredicateSpecification<T> other) {
127+
128+
Assert.notNull(other, "Other specification must not be null");
129+
130+
return SpecificationComposition.composed(this, where(other), CriteriaBuilder::or);
131+
}
132+
133+
/**
134+
* Negates the given {@link DeleteSpecification}.
135+
*
136+
* @param <T> the type of the {@link Root} the resulting {@literal DeleteSpecification} operates on.
137+
* @param spec can be {@literal null}.
138+
* @return guaranteed to be not {@literal null}.
139+
*/
140+
static <T> DeleteSpecification<T> not(DeleteSpecification<T> spec) {
141+
142+
Assert.notNull(spec, "Specification must not be null");
143+
144+
return (root, delete, builder) -> {
145+
146+
Predicate not = spec.toPredicate(root, delete, builder);
147+
return not != null ? builder.not(not) : null;
148+
};
149+
}
150+
151+
/**
152+
* Applies an AND operation to all the given {@link DeleteSpecification}s.
153+
*
154+
* @param specifications the {@link DeleteSpecification}s to compose.
155+
* @return the conjunction of the specifications.
156+
* @see #and(DeleteSpecification)
157+
* @see #allOf(Iterable)
158+
*/
159+
@SafeVarargs
160+
static <T> DeleteSpecification<T> allOf(DeleteSpecification<T>... specifications) {
161+
return allOf(Arrays.asList(specifications));
162+
}
163+
164+
/**
165+
* Applies an AND operation to all the given {@link DeleteSpecification}s.
166+
*
167+
* @param specifications the {@link DeleteSpecification}s to compose.
168+
* @return the conjunction of the specifications.
169+
* @see #and(DeleteSpecification)
170+
* @see #allOf(DeleteSpecification[])
171+
*/
172+
static <T> DeleteSpecification<T> allOf(Iterable<DeleteSpecification<T>> specifications) {
173+
174+
return StreamSupport.stream(specifications.spliterator(), false) //
175+
.reduce(DeleteSpecification.all(), DeleteSpecification::and);
176+
}
177+
178+
/**
179+
* Applies an OR operation to all the given {@link DeleteSpecification}s.
180+
*
181+
* @param specifications the {@link DeleteSpecification}s to compose.
182+
* @return the disjunction of the specifications.
183+
* @see #or(DeleteSpecification)
184+
* @see #anyOf(Iterable)
185+
*/
186+
@SafeVarargs
187+
static <T> DeleteSpecification<T> anyOf(DeleteSpecification<T>... specifications) {
188+
return anyOf(Arrays.asList(specifications));
189+
}
190+
191+
/**
192+
* Applies an OR operation to all the given {@link DeleteSpecification}s.
193+
*
194+
* @param specifications the {@link DeleteSpecification}s to compose.
195+
* @return the disjunction of the specifications.
196+
* @see #or(DeleteSpecification)
197+
* @see #anyOf(Iterable)
198+
*/
199+
static <T> DeleteSpecification<T> anyOf(Iterable<DeleteSpecification<T>> specifications) {
200+
201+
return StreamSupport.stream(specifications.spliterator(), false) //
202+
.reduce(DeleteSpecification.all(), DeleteSpecification::or);
203+
}
204+
205+
/**
206+
* Creates a WHERE clause for a query of the referenced entity in form of a {@link Predicate} for the given
207+
* {@link Root} and {@link CriteriaDelete}.
208+
*
209+
* @param root must not be {@literal null}.
210+
* @param delete the delete criteria.
211+
* @param criteriaBuilder must not be {@literal null}.
212+
* @return a {@link Predicate}, may be {@literal null}.
213+
*/
214+
@Nullable
215+
Predicate toPredicate(Root<T> root, CriteriaDelete<T> delete, CriteriaBuilder criteriaBuilder);
216+
217+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jpa.domain;
17+
18+
import jakarta.persistence.criteria.CriteriaBuilder;
19+
import jakarta.persistence.criteria.Predicate;
20+
import jakarta.persistence.criteria.Root;
21+
22+
import java.io.Serial;
23+
import java.io.Serializable;
24+
import java.util.Arrays;
25+
import java.util.stream.StreamSupport;
26+
27+
import org.springframework.lang.Nullable;
28+
import org.springframework.util.Assert;
29+
30+
/**
31+
* Specification in the sense of Domain Driven Design.
32+
*
33+
* @author Mark Paluch
34+
* @since xxx
35+
*/
36+
public interface PredicateSpecification<T> extends Serializable {
37+
38+
@Serial long serialVersionUID = 1L;
39+
40+
/**
41+
* Simple static factory method to create a specification matching all objects.
42+
*
43+
* @param <T> the type of the {@link Root} the resulting {@literal PredicateSpecification} operates on.
44+
* @return guaranteed to be not {@literal null}.
45+
*/
46+
static <T> PredicateSpecification<T> all() {
47+
return (root, builder) -> null;
48+
}
49+
50+
/**
51+
* Simple static factory method to add some syntactic sugar around a {@literal PredicateSpecification}.
52+
*
53+
* @param <T> the type of the {@link Root} the resulting {@literal PredicateSpecification} operates on.
54+
* @param spec must not be {@literal null}.
55+
* @return guaranteed to be not {@literal null}.
56+
* @since 2.0
57+
*/
58+
static <T> PredicateSpecification<T> where(PredicateSpecification<T> spec) {
59+
60+
Assert.notNull(spec, "DeleteSpecification must not be null");
61+
62+
return spec;
63+
}
64+
65+
/**
66+
* ANDs the given {@literal PredicateSpecification} to the current one.
67+
*
68+
* @param other the other {@link PredicateSpecification}.
69+
* @return the conjunction of the specifications.
70+
*/
71+
default PredicateSpecification<T> and(PredicateSpecification<T> other) {
72+
73+
Assert.notNull(other, "Other specification must not be null");
74+
75+
return SpecificationComposition.composed(this, other, CriteriaBuilder::and);
76+
}
77+
78+
/**
79+
* ORs the given specification to the current one.
80+
*
81+
* @param other the other {@link PredicateSpecification}.
82+
* @return the disjunction of the specifications.
83+
*/
84+
default PredicateSpecification<T> or(PredicateSpecification<T> other) {
85+
86+
Assert.notNull(other, "Other specification must not be null");
87+
88+
return SpecificationComposition.composed(this, other, CriteriaBuilder::or);
89+
}
90+
91+
/**
92+
* Negates the given {@link PredicateSpecification}.
93+
*
94+
* @param <T> the type of the {@link Root} the resulting {@literal PredicateSpecification} operates on.
95+
* @param spec can be {@literal null}.
96+
* @return guaranteed to be not {@literal null}.
97+
*/
98+
static <T> PredicateSpecification<T> not(PredicateSpecification<T> spec) {
99+
100+
Assert.notNull(spec, "Specification must not be null");
101+
102+
return (root, builder) -> {
103+
104+
Predicate not = spec.toPredicate(root, builder);
105+
return not != null ? builder.not(not) : null;
106+
};
107+
}
108+
109+
/**
110+
* Applies an AND operation to all the given {@link PredicateSpecification}s.
111+
*
112+
* @param specifications the {@link PredicateSpecification}s to compose.
113+
* @return the conjunction of the specifications.
114+
* @see #allOf(Iterable)
115+
* @see #and(PredicateSpecification)
116+
*/
117+
@SafeVarargs
118+
static <T> PredicateSpecification<T> allOf(PredicateSpecification<T>... specifications) {
119+
return allOf(Arrays.asList(specifications));
120+
}
121+
122+
/**
123+
* Applies an AND operation to all the given {@link PredicateSpecification}s.
124+
*
125+
* @param specifications the {@link PredicateSpecification}s to compose.
126+
* @return the conjunction of the specifications.
127+
* @see #and(PredicateSpecification)
128+
* @see #allOf(PredicateSpecification[])
129+
*/
130+
static <T> PredicateSpecification<T> allOf(Iterable<PredicateSpecification<T>> specifications) {
131+
132+
return StreamSupport.stream(specifications.spliterator(), false) //
133+
.reduce(PredicateSpecification.all(), PredicateSpecification::and);
134+
}
135+
136+
/**
137+
* Applies an OR operation to all the given {@link PredicateSpecification}s.
138+
*
139+
* @param specifications the {@link PredicateSpecification}s to compose.
140+
* @return the disjunction of the specifications.
141+
* @see #or(PredicateSpecification)
142+
* @see #anyOf(Iterable)
143+
*/
144+
@SafeVarargs
145+
static <T> PredicateSpecification<T> anyOf(PredicateSpecification<T>... specifications) {
146+
return anyOf(Arrays.asList(specifications));
147+
}
148+
149+
/**
150+
* Applies an OR operation to all the given {@link PredicateSpecification}s.
151+
*
152+
* @param specifications the {@link PredicateSpecification}s to compose.
153+
* @return the disjunction of the specifications.
154+
* @see #or(PredicateSpecification)
155+
* @see #anyOf(PredicateSpecification[])
156+
*/
157+
static <T> PredicateSpecification<T> anyOf(Iterable<PredicateSpecification<T>> specifications) {
158+
159+
return StreamSupport.stream(specifications.spliterator(), false) //
160+
.reduce(PredicateSpecification.all(), PredicateSpecification::or);
161+
}
162+
163+
/**
164+
* Creates a WHERE clause for a query of the referenced entity in form of a {@link Predicate} for the given
165+
* {@link Root} and {@link CriteriaBuilder}.
166+
*
167+
* @param root must not be {@literal null}.
168+
* @param criteriaBuilder must not be {@literal null}.
169+
* @return a {@link Predicate}, may be {@literal null}.
170+
*/
171+
@Nullable
172+
Predicate toPredicate(Root<T> root, CriteriaBuilder criteriaBuilder);
173+
174+
}

0 commit comments

Comments
 (0)