, U> Path dot(A attribute) {
* @param attribute must not be {@literal null}.
* @return
*/
+ @Contract("_ -> new")
+ @CheckReturnValue
public , U> Path dot(P attribute) {
- return new Path(add(attribute));
+ return new Path<>(add(attribute));
}
private List> add(Attribute, ?> attribute) {
Assert.notNull(attribute, "Attribute must not be null");
- List> newAttributes = new ArrayList>(attributes.size() + 1);
+ List> newAttributes = new ArrayList<>(attributes.size() + 1);
newAttributes.addAll(attributes);
newAttributes.add(attribute);
return newAttributes;
@@ -302,7 +320,7 @@ public String toString() {
builder.append(attribute.getName()).append(".");
}
- return builder.length() == 0 ? "" : builder.substring(0, builder.lastIndexOf("."));
+ return builder.isEmpty() ? "" : builder.substring(0, builder.lastIndexOf("."));
}
}
@@ -316,7 +334,7 @@ public String toString() {
*/
public static class JpaOrder extends Order {
- private static final long serialVersionUID = 1L;
+ @Serial private static final long serialVersionUID = 1L;
private final boolean unsafe;
@@ -325,7 +343,7 @@ public static class JpaOrder extends Order {
* {@link Sort#DEFAULT_DIRECTION}
*
* @param direction can be {@literal null}, will default to {@link Sort#DEFAULT_DIRECTION}.
- * @param property must not be {@literal null}.
+ * @param property must not be {@literal null}.
*/
private JpaOrder(@Nullable Direction direction, String property) {
this(direction, property, NullHandling.NATIVE);
@@ -335,8 +353,8 @@ private JpaOrder(@Nullable Direction direction, String property) {
* Creates a new {@link Order} instance. if order is {@literal null} then order defaults to
* {@link Sort#DEFAULT_DIRECTION}.
*
- * @param direction can be {@literal null}, will default to {@link Sort#DEFAULT_DIRECTION}.
- * @param property must not be {@literal null}.
+ * @param direction can be {@literal null}, will default to {@link Sort#DEFAULT_DIRECTION}.
+ * @param property must not be {@literal null}.
* @param nullHandlingHint can be {@literal null}, will default to {@link NullHandling#NATIVE}.
*/
private JpaOrder(@Nullable Direction direction, String property, NullHandling nullHandlingHint) {
@@ -344,7 +362,7 @@ private JpaOrder(@Nullable Direction direction, String property, NullHandling nu
}
private JpaOrder(@Nullable Direction direction, String property, boolean ignoreCase, NullHandling nullHandling,
- boolean unsafe) {
+ boolean unsafe) {
super(direction, property, ignoreCase, nullHandling);
this.unsafe = unsafe;
@@ -366,6 +384,8 @@ public JpaOrder with(NullHandling nullHandling) {
* @param properties must not be {@literal null}.
* @return
*/
+ @Contract("_ -> new")
+ @CheckReturnValue
public Sort withUnsafe(String... properties) {
Assert.notEmpty(properties, "Properties must not be empty");
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/PredicateSpecification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/PredicateSpecification.java
new file mode 100644
index 0000000000..318cf7c580
--- /dev/null
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/PredicateSpecification.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright 2024-2025 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.domain;
+
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.From;
+import jakarta.persistence.criteria.Predicate;
+
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.stream.StreamSupport;
+
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.lang.CheckReturnValue;
+import org.springframework.lang.Contract;
+import org.springframework.util.Assert;
+
+/**
+ * Specification in the sense of Domain Driven Design.
+ *
+ * Specifications can be composed into higher order functions from other specifications using
+ * {@link #and(PredicateSpecification)}, {@link #or(PredicateSpecification)} or factory methods such as
+ * {@link #allOf(Iterable)} with reduced type interference of the query source type.
+ *
+ * PredicateSpecifications are building blocks for composition and do not express their type opinion towards a specific
+ * entity source or join source type for improved reuse.
+ *
+ * Composition considers whether one or more specifications contribute to the overall predicate by returning a
+ * {@link Predicate} or {@literal null}. Specifications returning {@literal null}, such as {@link #unrestricted()}, are
+ * considered to not contribute to the overall predicate, and their result is not considered in the final predicate.
+ *
+ * @param the type of the {@link From From target} to which the specification is applied.
+ * @author Mark Paluch
+ * @author Peter Aisher
+ * @since 4.0
+ */
+@FunctionalInterface
+public interface PredicateSpecification extends Serializable {
+
+ /**
+ * Simple static factory method to create a specification which does not participate in matching. The specification
+ * returned is {@code null}-like, and is elided in all operations.
+ *
+ *
+ * unrestricted().and(other) // consider only `other`
+ * unrestricted().or(other) // consider only `other`
+ * not(unrestricted()) // equivalent to `unrestricted()`
+ *
+ *
+ * @param the type of the {@link From} the resulting {@literal PredicateSpecification} operates on.
+ * @return guaranteed to be not {@literal null}.
+ */
+ static PredicateSpecification unrestricted() {
+ return (from, builder) -> null;
+ }
+
+ /**
+ * Simple static factory method to add some syntactic sugar around a {@literal PredicateSpecification}.
+ *
+ * @param the type of the {@link From} the resulting {@literal PredicateSpecification} operates on.
+ * @param spec must not be {@literal null}.
+ * @return guaranteed to be not {@literal null}.
+ * @since 2.0
+ */
+ static PredicateSpecification where(PredicateSpecification spec) {
+
+ Assert.notNull(spec, "PredicateSpecification must not be null");
+
+ return spec;
+ }
+
+ /**
+ * ANDs the given {@literal PredicateSpecification} to the current one.
+ *
+ * @param other the other {@link PredicateSpecification}.
+ * @return the conjunction of the specifications.
+ */
+ @Contract("_ -> new")
+ @CheckReturnValue
+ default PredicateSpecification and(PredicateSpecification other) {
+
+ Assert.notNull(other, "Other specification must not be null");
+
+ return SpecificationComposition.composed(this, other, CriteriaBuilder::and);
+ }
+
+ /**
+ * ORs the given specification to the current one.
+ *
+ * @param other the other {@link PredicateSpecification}.
+ * @return the disjunction of the specifications.
+ */
+ @Contract("_ -> new")
+ @CheckReturnValue
+ default PredicateSpecification or(PredicateSpecification other) {
+
+ Assert.notNull(other, "Other specification must not be null");
+
+ return SpecificationComposition.composed(this, other, CriteriaBuilder::or);
+ }
+
+ /**
+ * Negates the given {@link PredicateSpecification}.
+ *
+ * @param the type of the {@link From} the resulting {@literal PredicateSpecification} operates on.
+ * @param spec can be {@literal null}.
+ * @return guaranteed to be not {@literal null}.
+ */
+ static PredicateSpecification not(PredicateSpecification spec) {
+
+ Assert.notNull(spec, "Specification must not be null");
+
+ return (from, builder) -> {
+
+ Predicate predicate = spec.toPredicate(from, builder);
+ return predicate != null ? builder.not(predicate) : null;
+ };
+ }
+
+ /**
+ * Applies an AND operation to all the given {@link PredicateSpecification}s. If {@code specifications} is empty, the
+ * resulting {@link PredicateSpecification} will be {@link #unrestricted()} applying to all objects.
+ *
+ * @param specifications the {@link PredicateSpecification}s to compose.
+ * @return the conjunction of the specifications.
+ * @see #allOf(Iterable)
+ * @see #and(PredicateSpecification)
+ */
+ @SafeVarargs
+ static PredicateSpecification allOf(PredicateSpecification... specifications) {
+ return allOf(Arrays.asList(specifications));
+ }
+
+ /**
+ * Applies an AND operation to all the given {@link PredicateSpecification}s. If {@code specifications} is empty, the
+ * resulting {@link PredicateSpecification} will be {@link #unrestricted()} applying to all objects.
+ *
+ * @param specifications the {@link PredicateSpecification}s to compose.
+ * @return the conjunction of the specifications.
+ * @see #and(PredicateSpecification)
+ * @see #allOf(PredicateSpecification[])
+ */
+ static PredicateSpecification allOf(Iterable> specifications) {
+
+ return StreamSupport.stream(specifications.spliterator(), false) //
+ .reduce(PredicateSpecification.unrestricted(), PredicateSpecification::and);
+ }
+
+ /**
+ * Applies an OR operation to all the given {@link PredicateSpecification}s. If {@code specifications} is empty, the
+ * resulting {@link PredicateSpecification} will be {@link #unrestricted()} applying to all objects.
+ *
+ * @param specifications the {@link PredicateSpecification}s to compose.
+ * @return the disjunction of the specifications.
+ * @see #or(PredicateSpecification)
+ * @see #anyOf(Iterable)
+ */
+ @SafeVarargs
+ static PredicateSpecification anyOf(PredicateSpecification... specifications) {
+ return anyOf(Arrays.asList(specifications));
+ }
+
+ /**
+ * Applies an OR operation to all the given {@link PredicateSpecification}s. If {@code specifications} is empty, the
+ * resulting {@link PredicateSpecification} will be {@link #unrestricted()} applying to all objects.
+ *
+ * @param specifications the {@link PredicateSpecification}s to compose.
+ * @return the disjunction of the specifications.
+ * @see #or(PredicateSpecification)
+ * @see #anyOf(PredicateSpecification[])
+ */
+ static PredicateSpecification anyOf(Iterable> specifications) {
+
+ return StreamSupport.stream(specifications.spliterator(), false) //
+ .reduce(PredicateSpecification.unrestricted(), PredicateSpecification::or);
+ }
+
+ /**
+ * Creates a WHERE clause for a query of the referenced entity in form of a {@link Predicate} for the given
+ * {@link From} and {@link CriteriaBuilder}.
+ *
+ * @param from must not be {@literal null}.
+ * @param criteriaBuilder must not be {@literal null}.
+ * @return a {@link Predicate}, may be {@literal null}.
+ */
+ @Nullable
+ Predicate toPredicate(From, T> from, CriteriaBuilder criteriaBuilder);
+
+}
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 635cd0d195..4e17dba2d5 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
@@ -1,5 +1,5 @@
/*
- * Copyright 2008-2024 the original author or authors.
+ * Copyright 2008-2025 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.
@@ -17,6 +17,7 @@
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
+import jakarta.persistence.criteria.CriteriaUpdate;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
@@ -24,11 +25,23 @@
import java.util.Arrays;
import java.util.stream.StreamSupport;
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.lang.CheckReturnValue;
+import org.springframework.lang.Contract;
+import org.springframework.util.Assert;
/**
* Specification in the sense of Domain Driven Design.
+ *
+ * Specifications can be composed into higher order functions from other specifications using
+ * {@link #and(Specification)}, {@link #or(Specification)} or factory methods such as {@link #allOf(Iterable)}.
+ *
+ * Composition considers whether one or more specifications contribute to the overall predicate by returning a
+ * {@link Predicate} or {@literal null}. Specifications returning {@literal null}, such as {@link #unrestricted()}, are
+ * considered to not contribute to the overall predicate, and their result is not considered in the final predicate.
*
+ * @param the type of the {@link Root entity} to which the specification is applied.
* @author Oliver Gierke
* @author Thomas Darimont
* @author Krzysztof Rzymkowski
@@ -37,87 +50,152 @@
* @author Jens Schauder
* @author Daniel Shuy
* @author Sergey Rukin
+ * @author Heeeun Cho
+ * @author Peter Aisher
*/
+@FunctionalInterface
public interface Specification extends Serializable {
- long serialVersionUID = 1L;
+ /**
+ * Simple static factory method to create a specification which does not participate in matching. The specification
+ * returned is {@code null}-like, and is elided in all operations.
+ *
+ *
+ * unrestricted().and(other) // consider only `other`
+ * unrestricted().or(other) // consider only `other`
+ * not(unrestricted()) // equivalent to `unrestricted()`
+ *
+ *
+ * @param the type of the {@link Root} the resulting {@literal Specification} operates on.
+ * @return guaranteed to be not {@literal null}.
+ * @since 4.0
+ */
+ static Specification unrestricted() {
+ return (root, query, builder) -> null;
+ }
/**
- * Negates the given {@link Specification}.
+ * Simple static factory method to add some syntactic sugar around a {@link Specification}.
*
+ * @implNote does not accept {@literal null} values since 4.0, use {@link #unrestricted()} instead of passing
+ * {@literal null} values.
* @param the type of the {@link Root} the resulting {@literal Specification} operates on.
* @param spec can be {@literal null}.
* @return guaranteed to be not {@literal null}.
* @since 2.0
*/
- static Specification