diff --git a/pom.xml b/pom.xml
index fd3739c6ec..58c09dad94 100755
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.springframework.data
spring-data-jpa-parent
- 4.0.0-SNAPSHOT
+ 4.0.0-GH-3521-SNAPSHOT
pom
Spring Data JPA Parent
diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml
index 0bdf2c8e7e..d12fd612fe 100755
--- a/spring-data-envers/pom.xml
+++ b/spring-data-envers/pom.xml
@@ -5,12 +5,12 @@
org.springframework.data
spring-data-envers
- 4.0.0-SNAPSHOT
+ 4.0.0-GH-3521-SNAPSHOT
org.springframework.data
spring-data-jpa-parent
- 4.0.0-SNAPSHOT
+ 4.0.0-GH-3521-SNAPSHOT
../pom.xml
diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml
index af5244a230..8763de2faa 100644
--- a/spring-data-jpa-distribution/pom.xml
+++ b/spring-data-jpa-distribution/pom.xml
@@ -14,7 +14,7 @@
org.springframework.data
spring-data-jpa-parent
- 4.0.0-SNAPSHOT
+ 4.0.0-GH-3521-SNAPSHOT
../pom.xml
diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml
index fcdd23640d..70f8fbbed7 100644
--- a/spring-data-jpa/pom.xml
+++ b/spring-data-jpa/pom.xml
@@ -6,7 +6,7 @@
org.springframework.data
spring-data-jpa
- 4.0.0-SNAPSHOT
+ 4.0.0-GH-3521-SNAPSHOT
Spring Data JPA
Spring Data module for JPA repositories.
@@ -15,7 +15,7 @@
org.springframework.data
spring-data-jpa-parent
- 4.0.0-SNAPSHOT
+ 4.0.0-GH-3521-SNAPSHOT
../pom.xml
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/DeleteSpecification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/DeleteSpecification.java
new file mode 100644
index 0000000000..3337ae5fb1
--- /dev/null
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/DeleteSpecification.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright 2024 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.CriteriaDelete;
+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.CheckReturnValue;
+import org.springframework.lang.Contract;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+
+/**
+ * Specification in the sense of Domain Driven Design to handle Criteria Deletes.
+ *
+ * Specifications can be composed into higher order functions from other specifications using
+ * {@link #and(DeleteSpecification)}, {@link #or(DeleteSpecification)} 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} are
+ * considered to not contribute to the overall predicate and their result is not considered in the final predicate.
+ *
+ * @author Mark Paluch
+ * @since 4.0
+ */
+@FunctionalInterface
+public interface DeleteSpecification extends Serializable {
+
+ /**
+ * Simple static factory method to create a specification deleting all objects.
+ *
+ * @param the type of the {@link Root} the resulting {@literal DeleteSpecification} operates on.
+ * @return guaranteed to be not {@literal null}.
+ */
+ static DeleteSpecification unrestricted() {
+ return (root, query, builder) -> null;
+ }
+
+ /**
+ * Simple static factory method to add some syntactic sugar around a {@literal DeleteSpecification}.
+ *
+ * @param the type of the {@link Root} the resulting {@literal DeleteSpecification} operates on.
+ * @param spec must not be {@literal null}.
+ * @return guaranteed to be not {@literal null}.
+ */
+ static DeleteSpecification where(DeleteSpecification spec) {
+
+ Assert.notNull(spec, "DeleteSpecification must not be null");
+
+ return spec;
+ }
+
+ /**
+ * Simple static factory method to add some syntactic sugar translating {@link PredicateSpecification} to
+ * {@link DeleteSpecification}.
+ *
+ * @param the type of the {@link Root} the resulting {@literal DeleteSpecification} operates on.
+ * @param spec the {@link PredicateSpecification} to wrap.
+ * @return guaranteed to be not {@literal null}.
+ */
+ static DeleteSpecification where(PredicateSpecification spec) {
+
+ Assert.notNull(spec, "PredicateSpecification must not be null");
+
+ return where((root, delete, criteriaBuilder) -> spec.toPredicate(root, criteriaBuilder));
+ }
+
+ /**
+ * ANDs the given {@link DeleteSpecification} to the current one.
+ *
+ * @param other the other {@link DeleteSpecification}.
+ * @return the conjunction of the specifications.
+ */
+ @Contract("_ -> new")
+ @CheckReturnValue
+ default DeleteSpecification and(DeleteSpecification other) {
+
+ Assert.notNull(other, "Other specification must not be null");
+
+ return SpecificationComposition.composed(this, other, CriteriaBuilder::and);
+ }
+
+ /**
+ * ANDs the given {@link DeleteSpecification} to the current one.
+ *
+ * @param other the other {@link PredicateSpecification}.
+ * @return the conjunction of the specifications.
+ */
+ @Contract("_ -> new")
+ @CheckReturnValue
+ default DeleteSpecification and(PredicateSpecification other) {
+
+ Assert.notNull(other, "Other specification must not be null");
+
+ return SpecificationComposition.composed(this, where(other), CriteriaBuilder::and);
+ }
+
+ /**
+ * ORs the given specification to the current one.
+ *
+ * @param other the other {@link DeleteSpecification}.
+ * @return the disjunction of the specifications.
+ */
+ @Contract("_ -> new")
+ @CheckReturnValue
+ default DeleteSpecification or(DeleteSpecification other) {
+
+ Assert.notNull(other, "Other specification must not be null");
+
+ return SpecificationComposition.composed(this, other, CriteriaBuilder::or);
+ }
+
+ /**
+ * ORs the given specification to the current one.
+ *
+ * @param other the other {@link PredicateSpecification}.
+ * @return the disjunction of the specifications.
+ */
+ @Contract("_ -> new")
+ @CheckReturnValue
+ default DeleteSpecification or(PredicateSpecification other) {
+
+ Assert.notNull(other, "Other specification must not be null");
+
+ return SpecificationComposition.composed(this, where(other), CriteriaBuilder::or);
+ }
+
+ /**
+ * Negates the given {@link DeleteSpecification}.
+ *
+ * @param the type of the {@link Root} the resulting {@literal DeleteSpecification} operates on.
+ * @param spec can be {@literal null}.
+ * @return guaranteed to be not {@literal null}.
+ */
+ static DeleteSpecification not(DeleteSpecification spec) {
+
+ Assert.notNull(spec, "Specification must not be null");
+
+ return (root, delete, builder) -> {
+
+ Predicate predicate = spec.toPredicate(root, delete, builder);
+ return predicate != null ? builder.not(predicate) : null;
+ };
+ }
+
+ /**
+ * Applies an AND operation to all the given {@link DeleteSpecification}s. If {@code specifications} is empty, the
+ * resulting {@link DeleteSpecification} will be unrestricted applying to all objects.
+ *
+ * @param specifications the {@link DeleteSpecification}s to compose.
+ * @return the conjunction of the specifications.
+ * @see #and(DeleteSpecification)
+ * @see #allOf(Iterable)
+ */
+ @SafeVarargs
+ static DeleteSpecification allOf(DeleteSpecification... specifications) {
+ return allOf(Arrays.asList(specifications));
+ }
+
+ /**
+ * Applies an AND operation to all the given {@link DeleteSpecification}s. If {@code specifications} is empty, the
+ * resulting {@link DeleteSpecification} will be unrestricted applying to all objects.
+ *
+ * @param specifications the {@link DeleteSpecification}s to compose.
+ * @return the conjunction of the specifications.
+ * @see #and(DeleteSpecification)
+ * @see #allOf(DeleteSpecification[])
+ */
+ static DeleteSpecification allOf(Iterable> specifications) {
+
+ return StreamSupport.stream(specifications.spliterator(), false) //
+ .reduce(DeleteSpecification.unrestricted(), DeleteSpecification::and);
+ }
+
+ /**
+ * Applies an OR operation to all the given {@link DeleteSpecification}s. If {@code specifications} is empty, the
+ * resulting {@link DeleteSpecification} will be unrestricted applying to all objects.
+ *
+ * @param specifications the {@link DeleteSpecification}s to compose.
+ * @return the disjunction of the specifications.
+ * @see #or(DeleteSpecification)
+ * @see #anyOf(Iterable)
+ */
+ @SafeVarargs
+ static DeleteSpecification anyOf(DeleteSpecification... specifications) {
+ return anyOf(Arrays.asList(specifications));
+ }
+
+ /**
+ * Applies an OR operation to all the given {@link DeleteSpecification}s. If {@code specifications} is empty, the
+ * resulting {@link DeleteSpecification} will be unrestricted applying to all objects.
+ *
+ * @param specifications the {@link DeleteSpecification}s to compose.
+ * @return the disjunction of the specifications.
+ * @see #or(DeleteSpecification)
+ * @see #anyOf(Iterable)
+ */
+ static DeleteSpecification anyOf(Iterable> specifications) {
+
+ return StreamSupport.stream(specifications.spliterator(), false) //
+ .reduce(DeleteSpecification.unrestricted(), DeleteSpecification::or);
+ }
+
+ /**
+ * Creates a WHERE clause for a query of the referenced entity in form of a {@link Predicate} for the given
+ * {@link Root} and {@link CriteriaDelete}.
+ *
+ * @param root must not be {@literal null}.
+ * @param delete the delete criteria.
+ * @param criteriaBuilder must not be {@literal null}.
+ * @return a {@link Predicate}, may be {@literal null}.
+ */
+ @Nullable
+ Predicate toPredicate(Root root, CriteriaDelete delete, CriteriaBuilder criteriaBuilder);
+
+}
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..dc17edbfc4
--- /dev/null
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/PredicateSpecification.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2024 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.Predicate;
+import jakarta.persistence.criteria.Root;
+
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.stream.StreamSupport;
+
+import org.springframework.lang.CheckReturnValue;
+import org.springframework.lang.Contract;
+import org.springframework.lang.Nullable;
+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)}. Composition considers whether one or more specifications contribute to the overall
+ * predicate by returning a {@link Predicate} or {@literal null}. Specifications returning {@literal null} are
+ * considered to not contribute to the overall predicate and their result is not considered in the final predicate.
+ *
+ * @author Mark Paluch
+ * @since 4.0
+ */
+public interface PredicateSpecification extends Serializable {
+
+ /**
+ * Simple static factory method to create a specification matching all objects.
+ *
+ * @param the type of the {@link Root} the resulting {@literal PredicateSpecification} operates on.
+ * @return guaranteed to be not {@literal null}.
+ */
+ static PredicateSpecification unrestricted() {
+ return (root, builder) -> null;
+ }
+
+ /**
+ * Simple static factory method to add some syntactic sugar around a {@literal PredicateSpecification}.
+ *
+ * @param the type of the {@link Root} 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 Root} 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 (root, builder) -> {
+
+ Predicate predicate = spec.toPredicate(root, 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 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 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 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 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 Root} and {@link CriteriaBuilder}.
+ *
+ * @param root must not be {@literal null}.
+ * @param criteriaBuilder must not be {@literal null}.
+ * @return a {@link Predicate}, may be {@literal null}.
+ */
+ @Nullable
+ Predicate toPredicate(Root root, 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 ea626af591..b0b44dc0f6 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
@@ -17,18 +17,27 @@
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;
-import java.io.Serial;
import java.io.Serializable;
import java.util.Arrays;
import java.util.stream.StreamSupport;
+import org.springframework.lang.CheckReturnValue;
+import org.springframework.lang.Contract;
import org.springframework.lang.Nullable;
+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} are considered to not contribute to
+ * the overall predicate and their result is not considered in the final predicate.
*
* @author Oliver Gierke
* @author Thomas Darimont
@@ -39,86 +48,124 @@
* @author Daniel Shuy
* @author Sergey Rukin
*/
+@FunctionalInterface
public interface Specification extends Serializable {
- @Serial long serialVersionUID = 1L;
-
/**
- * Negates the given {@link Specification}.
+ * Simple static factory method to create a specification matching all objects.
*
* @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 not(@Nullable Specification spec) {
-
- return spec == null //
- ? (root, query, builder) -> null //
- : (root, query, builder) -> builder.not(spec.toPredicate(root, query, builder));
+ static Specification unrestricted() {
+ return (root, query, builder) -> null;
}
/**
- * Simple static factory method to add some syntactic sugar around a {@link Specification}.
+ * Simple static factory method to add some syntactic sugar translating {@link PredicateSpecification} to
+ * {@link Specification}.
*
* @param the type of the {@link Root} the resulting {@literal Specification} operates on.
- * @param spec can be {@literal null}.
+ * @param spec the {@link PredicateSpecification} to wrap.
* @return guaranteed to be not {@literal null}.
- * @since 2.0
*/
- static Specification where(@Nullable Specification spec) {
- return spec == null ? (root, query, builder) -> null : spec;
+ static Specification where(PredicateSpecification spec) {
+
+ Assert.notNull(spec, "PredicateSpecification must not be null");
+
+ return (root, update, criteriaBuilder) -> spec.toPredicate(root, criteriaBuilder);
}
/**
* ANDs the given {@link Specification} to the current one.
*
- * @param other can be {@literal null}.
- * @return The conjunction of the specifications
+ * @param other the other {@link Specification}.
+ * @return the conjunction of the specifications.
* @since 2.0
*/
- default Specification and(@Nullable Specification other) {
+ @Contract("_ -> new")
+ @CheckReturnValue
+ default Specification and(Specification other) {
+
+ Assert.notNull(other, "Other specification must not be null");
+
return SpecificationComposition.composed(this, other, CriteriaBuilder::and);
}
+ /**
+ * ANDs the given {@link Specification} to the current one.
+ *
+ * @param other the other {@link PredicateSpecification}.
+ * @return the conjunction of the specifications.
+ * @since 2.0
+ */
+ @Contract("_ -> new")
+ @CheckReturnValue
+ default Specification and(PredicateSpecification other) {
+
+ Assert.notNull(other, "Other specification must not be null");
+
+ return SpecificationComposition.composed(this, where(other), CriteriaBuilder::and);
+ }
+
/**
* ORs the given specification to the current one.
*
- * @param other can be {@literal null}.
- * @return The disjunction of the specifications
+ * @param other the other {@link Specification}.
+ * @return the disjunction of the specifications
* @since 2.0
*/
- default Specification or(@Nullable Specification other) {
+ @Contract("_ -> new")
+ @CheckReturnValue
+ default Specification or(Specification other) {
+
+ Assert.notNull(other, "Other specification must not be null");
+
return SpecificationComposition.composed(this, other, CriteriaBuilder::or);
}
/**
- * Creates a WHERE clause for a query of the referenced entity in form of a {@link Predicate} for the given
- * {@link Root} and {@link CriteriaQuery}.
+ * ORs the given specification to the current one.
*
- * @param root must not be {@literal null}.
- * @param query can be {@literal null} to allow overrides that accept {@link jakarta.persistence.criteria.CriteriaDelete} which is an {@link jakarta.persistence.criteria.AbstractQuery} but no {@link CriteriaQuery}.
- * @param criteriaBuilder must not be {@literal null}.
- * @return a {@link Predicate}, may be {@literal null}.
+ * @param other the other {@link PredicateSpecification}.
+ * @return the disjunction of the specifications
+ * @since 2.0
*/
- @Nullable
- Predicate toPredicate(Root root, @Nullable CriteriaQuery> query, CriteriaBuilder criteriaBuilder);
+ @Contract("_ -> new")
+ @CheckReturnValue
+ default Specification or(PredicateSpecification other) {
+
+ Assert.notNull(other, "Other specification must not be null");
+
+ return SpecificationComposition.composed(this, where(other), CriteriaBuilder::or);
+ }
/**
- * Applies an AND operation to all the given {@link Specification}s.
+ * Negates the given {@link Specification}.
*
- * @param specifications The {@link Specification}s to compose. Can contain {@code null}s.
- * @return The conjunction of the specifications
- * @see #and(Specification)
- * @since 3.0
+ * @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 allOf(Iterable> specifications) {
+ static Specification not(Specification spec) {
- return StreamSupport.stream(specifications.spliterator(), false) //
- .reduce(Specification.where(null), Specification::and);
+ Assert.notNull(spec, "Specification must not be null");
+
+ return (root, query, builder) -> {
+
+ Predicate predicate = spec.toPredicate(root, query, builder);
+ return predicate != null ? builder.not(predicate) : null;
+ };
}
/**
+ * Applies an AND operation to all the given {@link Specification}s. If {@code specifications} is empty, the resulting
+ * {@link Specification} will be unrestricted applying to all objects.
+ *
+ * @param specifications the {@link Specification}s to compose.
+ * @return the conjunction of the specifications.
+ * @see #and(Specification)
* @see #allOf(Iterable)
* @since 3.0
*/
@@ -128,20 +175,28 @@ static Specification allOf(Specification... specifications) {
}
/**
- * Applies an OR operation to all the given {@link Specification}s.
+ * Applies an AND operation to all the given {@link Specification}s. If {@code specifications} is empty, the resulting
+ * {@link Specification} will be unrestricted applying to all objects.
*
- * @param specifications The {@link Specification}s to compose. Can contain {@code null}s.
- * @return The disjunction of the specifications
- * @see #or(Specification)
+ * @param specifications the {@link Specification}s to compose.
+ * @return the conjunction of the specifications.
+ * @see #and(Specification)
+ * @see #allOf(Specification[])
* @since 3.0
*/
- static Specification anyOf(Iterable> specifications) {
+ static Specification allOf(Iterable> specifications) {
return StreamSupport.stream(specifications.spliterator(), false) //
- .reduce(Specification.where(null), Specification::or);
+ .reduce(Specification.unrestricted(), Specification::and);
}
/**
+ * Applies an OR operation to all the given {@link Specification}s. If {@code specifications} is empty, the resulting
+ * {@link Specification} will be unrestricted applying to all objects.
+ *
+ * @param specifications the {@link Specification}s to compose.
+ * @return the disjunction of the specifications
+ * @see #or(Specification)
* @see #anyOf(Iterable)
* @since 3.0
*/
@@ -149,4 +204,33 @@ static Specification anyOf(Iterable> specifications) {
static Specification anyOf(Specification... specifications) {
return anyOf(Arrays.asList(specifications));
}
+
+ /**
+ * Applies an OR operation to all the given {@link Specification}s. If {@code specifications} is empty, the resulting
+ * {@link Specification} will be unrestricted applying to all objects.
+ *
+ * @param specifications the {@link Specification}s to compose.
+ * @return the disjunction of the specifications
+ * @see #or(Specification)
+ * @see #anyOf(Iterable)
+ * @since 3.0
+ */
+ static Specification anyOf(Iterable> specifications) {
+
+ return StreamSupport.stream(specifications.spliterator(), false) //
+ .reduce(Specification.unrestricted(), Specification::or);
+ }
+
+ /**
+ * Creates a WHERE clause for a query of the referenced entity in form of a {@link Predicate} for the given
+ * {@link Root} and {@link CriteriaUpdate}.
+ *
+ * @param root must not be {@literal null}.
+ * @param query the criteria query.
+ * @param criteriaBuilder must not be {@literal null}.
+ * @return a {@link Predicate}, may be {@literal null}.
+ */
+ @Nullable
+ Predicate toPredicate(Root root, CriteriaQuery> query, CriteriaBuilder criteriaBuilder);
+
}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/SpecificationComposition.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/SpecificationComposition.java
index ad78749e39..0b6e90014c 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/SpecificationComposition.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/SpecificationComposition.java
@@ -15,13 +15,15 @@
*/
package org.springframework.data.jpa.domain;
-import java.io.Serializable;
-
import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.CriteriaDelete;
import jakarta.persistence.criteria.CriteriaQuery;
+import jakarta.persistence.criteria.CriteriaUpdate;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
+import java.io.Serializable;
+
import org.springframework.lang.Nullable;
/**
@@ -57,8 +59,75 @@ static Specification composed(@Nullable Specification lhs, @Nullable S
}
@Nullable
- private static Predicate toPredicate(@Nullable Specification specification, Root root, @Nullable CriteriaQuery> query,
- CriteriaBuilder builder) {
+ private static Predicate toPredicate(@Nullable Specification specification, Root root,
+ @Nullable CriteriaQuery> query, CriteriaBuilder builder) {
return specification == null ? null : specification.toPredicate(root, query, builder);
}
+
+ static DeleteSpecification composed(@Nullable DeleteSpecification lhs, @Nullable DeleteSpecification rhs,
+ Combiner combiner) {
+
+ return (root, query, builder) -> {
+
+ Predicate thisPredicate = toPredicate(lhs, root, query, builder);
+ Predicate otherPredicate = toPredicate(rhs, root, query, builder);
+
+ if (thisPredicate == null) {
+ return otherPredicate;
+ }
+
+ return otherPredicate == null ? thisPredicate : combiner.combine(builder, thisPredicate, otherPredicate);
+ };
+ }
+
+ @Nullable
+ private static Predicate toPredicate(@Nullable DeleteSpecification specification, Root root,
+ @Nullable CriteriaDelete delete, CriteriaBuilder builder) {
+ return specification == null ? null : specification.toPredicate(root, delete, builder);
+ }
+
+ static UpdateSpecification composed(@Nullable UpdateSpecification lhs, @Nullable UpdateSpecification rhs,
+ Combiner combiner) {
+
+ return (root, query, builder) -> {
+
+ Predicate thisPredicate = toPredicate(lhs, root, query, builder);
+ Predicate otherPredicate = toPredicate(rhs, root, query, builder);
+
+ if (thisPredicate == null) {
+ return otherPredicate;
+ }
+
+ return otherPredicate == null ? thisPredicate : combiner.combine(builder, thisPredicate, otherPredicate);
+ };
+ }
+
+ @Nullable
+ private static Predicate toPredicate(@Nullable UpdateSpecification specification, Root root,
+ CriteriaUpdate update, CriteriaBuilder builder) {
+ return specification == null ? null : specification.toPredicate(root, update, builder);
+ }
+
+ static PredicateSpecification composed(PredicateSpecification lhs, PredicateSpecification rhs,
+ Combiner combiner) {
+
+ return (root, builder) -> {
+
+ Predicate thisPredicate = toPredicate(lhs, root, builder);
+ Predicate otherPredicate = toPredicate(rhs, root, builder);
+
+ if (thisPredicate == null) {
+ return otherPredicate;
+ }
+
+ return otherPredicate == null ? thisPredicate : combiner.combine(builder, thisPredicate, otherPredicate);
+ };
+ }
+
+ @Nullable
+ private static Predicate toPredicate(@Nullable PredicateSpecification specification, Root root,
+ CriteriaBuilder builder) {
+ return specification == null ? null : specification.toPredicate(root, builder);
+ }
+
}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/UpdateSpecification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/UpdateSpecification.java
new file mode 100644
index 0000000000..2e9d93b82a
--- /dev/null
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/UpdateSpecification.java
@@ -0,0 +1,338 @@
+/*
+ * Copyright 2024 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.CriteriaUpdate;
+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.CheckReturnValue;
+import org.springframework.lang.Contract;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+
+/**
+ * Specification in the sense of Domain Driven Design to handle Criteria Updates.
+ *
+ * Specifications can be composed into higher order functions from other specifications using
+ * {@link #and(UpdateSpecification)}, {@link #or(UpdateSpecification)} 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} are
+ * considered to not contribute to the overall predicate and their result is not considered in the final predicate.
+ *
+ * @author Mark Paluch
+ * @since 4.0
+ */
+@FunctionalInterface
+public interface UpdateSpecification extends Serializable {
+
+ /**
+ * Simple static factory method to create a specification updating all objects.
+ *
+ * @param the type of the {@link Root} the resulting {@literal UpdateSpecification} operates on.
+ * @return guaranteed to be not {@literal null}.
+ */
+ static UpdateSpecification unrestricted() {
+ return (root, query, builder) -> null;
+ }
+
+ /**
+ * Simple static factory method to add some syntactic sugar around a {@literal UpdateOperation}. For example:
+ *
+ *
+ * UpdateSpecification<User> updateLastname = UpdateOperation
+ * .<User> update((root, update, criteriaBuilder) -> update.set("lastname", "Heisenberg"))
+ * .where(userHasFirstname("Walter").and(userHasLastname("White")));
+ *
+ * repository.update(updateLastname);
+ *
+ *
+ * @param the type of the {@link Root} the resulting {@literal UpdateOperation} operates on.
+ * @param spec must not be {@literal null}.
+ * @return guaranteed to be not {@literal null}.
+ */
+ static UpdateOperation update(UpdateOperation spec) {
+
+ Assert.notNull(spec, "UpdateSpecification must not be null");
+
+ return spec;
+ }
+
+ /**
+ * Simple static factory method to add some syntactic sugar around a {@literal UpdateSpecification}.
+ *
+ * @param the type of the {@link Root} the resulting {@literal UpdateSpecification} operates on.
+ * @param spec must not be {@literal null}.
+ * @return guaranteed to be not {@literal null}.
+ */
+ static UpdateSpecification where(UpdateSpecification spec) {
+
+ Assert.notNull(spec, "UpdateSpecification must not be null");
+
+ return spec;
+ }
+
+ /**
+ * Simple static factory method to add some syntactic sugar translating {@link PredicateSpecification} to
+ * {@link UpdateSpecification}.
+ *
+ * @param the type of the {@link Root} the resulting {@literal UpdateSpecification} operates on.
+ * @param spec the {@link PredicateSpecification} to wrap.
+ * @return guaranteed to be not {@literal null}.
+ */
+ static UpdateSpecification where(PredicateSpecification spec) {
+
+ Assert.notNull(spec, "PredicateSpecification must not be null");
+
+ return where((root, update, criteriaBuilder) -> spec.toPredicate(root, criteriaBuilder));
+ }
+
+ /**
+ * ANDs the given {@link UpdateSpecification} to the current one.
+ *
+ * @param other the other {@link UpdateSpecification}.
+ * @return the conjunction of the specifications.
+ */
+ @Contract("_ -> new")
+ @CheckReturnValue
+ default UpdateSpecification and(UpdateSpecification other) {
+
+ Assert.notNull(other, "Other specification must not be null");
+
+ return SpecificationComposition.composed(this, other, CriteriaBuilder::and);
+ }
+
+ /**
+ * ANDs the given {@link UpdateSpecification} to the current one.
+ *
+ * @param other the other {@link PredicateSpecification}.
+ * @return the conjunction of the specifications.
+ */
+ @Contract("_ -> new")
+ @CheckReturnValue
+ default UpdateSpecification and(PredicateSpecification other) {
+
+ Assert.notNull(other, "Other specification must not be null");
+
+ return SpecificationComposition.composed(this, where(other), CriteriaBuilder::and);
+ }
+
+ /**
+ * ORs the given specification to the current one.
+ *
+ * @param other the other {@link UpdateSpecification}.
+ * @return the disjunction of the specifications.
+ */
+ @Contract("_ -> new")
+ @CheckReturnValue
+ default UpdateSpecification or(UpdateSpecification other) {
+
+ Assert.notNull(other, "Other specification must not be null");
+
+ return SpecificationComposition.composed(this, other, CriteriaBuilder::or);
+ }
+
+ /**
+ * ORs the given specification to the current one.
+ *
+ * @param other the other {@link PredicateSpecification}.
+ * @return the disjunction of the specifications.
+ */
+ @Contract("_ -> new")
+ @CheckReturnValue
+ default UpdateSpecification or(PredicateSpecification other) {
+
+ Assert.notNull(other, "Other specification must not be null");
+
+ return SpecificationComposition.composed(this, where(other), CriteriaBuilder::or);
+ }
+
+ /**
+ * Negates the given {@link UpdateSpecification}.
+ *
+ * @param the type of the {@link Root} the resulting {@literal UpdateSpecification} operates on.
+ * @param spec can be {@literal null}.
+ * @return guaranteed to be not {@literal null}.
+ */
+ static UpdateSpecification not(UpdateSpecification spec) {
+
+ Assert.notNull(spec, "Specification must not be null");
+
+ return (root, update, builder) -> {
+
+ Predicate predicate = spec.toPredicate(root, update, builder);
+ return predicate != null ? builder.not(predicate) : null;
+ };
+ }
+
+ /**
+ * Applies an AND operation to all the given {@link UpdateSpecification}s. If {@code specifications} is empty, the
+ * resulting {@link UpdateSpecification} will be unrestricted applying to all objects.
+ *
+ * @param specifications the {@link UpdateSpecification}s to compose.
+ * @return the conjunction of the specifications.
+ * @see #and(UpdateSpecification)
+ * @see #allOf(Iterable)
+ */
+ @SafeVarargs
+ static UpdateSpecification allOf(UpdateSpecification... specifications) {
+ return allOf(Arrays.asList(specifications));
+ }
+
+ /**
+ * Applies an AND operation to all the given {@link UpdateSpecification}s. If {@code specifications} is empty, the
+ * resulting {@link UpdateSpecification} will be unrestricted applying to all objects.
+ *
+ * @param specifications the {@link UpdateSpecification}s to compose.
+ * @return the conjunction of the specifications.
+ * @see #and(UpdateSpecification)
+ * @see #allOf(UpdateSpecification[])
+ */
+ static UpdateSpecification allOf(Iterable> specifications) {
+
+ return StreamSupport.stream(specifications.spliterator(), false) //
+ .reduce(UpdateSpecification.unrestricted(), UpdateSpecification::and);
+ }
+
+ /**
+ * Applies an OR operation to all the given {@link UpdateSpecification}s. If {@code specifications} is empty, the
+ * resulting {@link UpdateSpecification} will be unrestricted applying to all objects.
+ *
+ * @param specifications the {@link UpdateSpecification}s to compose.
+ * @return the disjunction of the specifications.
+ * @see #or(UpdateSpecification)
+ * @see #anyOf(Iterable)
+ */
+ @SafeVarargs
+ static UpdateSpecification anyOf(UpdateSpecification... specifications) {
+ return anyOf(Arrays.asList(specifications));
+ }
+
+ /**
+ * Applies an OR operation to all the given {@link UpdateSpecification}s. If {@code specifications} is empty, the
+ * resulting {@link UpdateSpecification} will be unrestricted applying to all objects.
+ *
+ * @param specifications the {@link UpdateSpecification}s to compose.
+ * @return the disjunction of the specifications.
+ * @see #or(UpdateSpecification)
+ * @see #anyOf(Iterable)
+ */
+ static UpdateSpecification anyOf(Iterable> specifications) {
+
+ return StreamSupport.stream(specifications.spliterator(), false) //
+ .reduce(UpdateSpecification.unrestricted(), UpdateSpecification::or);
+ }
+
+ /**
+ * Creates a WHERE clause for a query of the referenced entity in form of a {@link Predicate} for the given
+ * {@link Root} and {@link CriteriaUpdate}.
+ *
+ * @param root must not be {@literal null}.
+ * @param update the update criteria.
+ * @param criteriaBuilder must not be {@literal null}.
+ * @return a {@link Predicate}, may be {@literal null}.
+ */
+ @Nullable
+ Predicate toPredicate(Root root, CriteriaUpdate update, CriteriaBuilder criteriaBuilder);
+
+ /**
+ * Simplified extension to {@link UpdateSpecification} that only considers the {@code UPDATE} part without specifying
+ * a predicate. This is useful to separate concerns for reusable specifications, for example:
+ *
+ *
+ * UpdateSpecification<User> updateLastname = UpdateSpecification
+ * .<User> update((root, update, criteriaBuilder) -> update.set("lastname", "Heisenberg"))
+ * .where(userHasFirstname("Walter").and(userHasLastname("White")));
+ *
+ * repository.update(updateLastname);
+ *
+ *
+ * @param
+ */
+ @FunctionalInterface
+ interface UpdateOperation {
+
+ /**
+ * ANDs the given {@link UpdateOperation} to the current one.
+ *
+ * @param other the other {@link UpdateOperation}.
+ * @return the conjunction of the specifications.
+ */
+ @Contract("_ -> new")
+ @CheckReturnValue
+ default UpdateOperation and(UpdateOperation other) {
+
+ Assert.notNull(other, "Other UpdateOperation must not be null");
+
+ return (root, update, criteriaBuilder) -> {
+ this.apply(root, update, criteriaBuilder);
+ other.apply(root, update, criteriaBuilder);
+ };
+ }
+
+ /**
+ * Creates a {@link UpdateSpecification} from this and the given {@link UpdateSpecification}.
+ *
+ * @param specification the {@link PredicateSpecification}.
+ * @return the conjunction of the specifications.
+ */
+ @Contract("_ -> new")
+ @CheckReturnValue
+ default UpdateSpecification where(PredicateSpecification specification) {
+
+ Assert.notNull(specification, "PredicateSpecification must not be null");
+
+ return (root, update, criteriaBuilder) -> {
+ this.apply(root, update, criteriaBuilder);
+ return specification.toPredicate(root, criteriaBuilder);
+ };
+ }
+
+ /**
+ * Creates a {@link UpdateSpecification} from this and the given {@link UpdateSpecification}.
+ *
+ * @param specification the {@link UpdateSpecification}.
+ * @return the conjunction of the specifications.
+ */
+ @Contract("_ -> new")
+ @CheckReturnValue
+ default UpdateSpecification where(UpdateSpecification specification) {
+
+ Assert.notNull(specification, "UpdateSpecification must not be null");
+
+ return (root, update, criteriaBuilder) -> {
+ this.apply(root, update, criteriaBuilder);
+ return specification.toPredicate(root, update, criteriaBuilder);
+ };
+ }
+
+ /**
+ * Accept the given {@link Root} and {@link CriteriaUpdate} to apply the update operation.
+ *
+ * @param root must not be {@literal null}.
+ * @param update the update criteria.
+ * @param criteriaBuilder must not be {@literal null}.
+ */
+ void apply(Root root, CriteriaUpdate update, CriteriaBuilder criteriaBuilder);
+
+ }
+
+}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java
index 3abd83b2bf..ec32ec4e77 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java
@@ -15,10 +15,6 @@
*/
package org.springframework.data.jpa.repository;
-import jakarta.persistence.criteria.CriteriaBuilder;
-import jakarta.persistence.criteria.CriteriaQuery;
-import jakarta.persistence.criteria.Root;
-
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
@@ -26,9 +22,11 @@
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
+import org.springframework.data.jpa.domain.DeleteSpecification;
+import org.springframework.data.jpa.domain.PredicateSpecification;
import org.springframework.data.jpa.domain.Specification;
+import org.springframework.data.jpa.domain.UpdateSpecification;
import org.springframework.data.repository.query.FluentQuery;
-import org.springframework.lang.Nullable;
/**
* Interface to allow execution of {@link Specification}s based on the JPA criteria API.
@@ -37,87 +35,171 @@
* @author Christoph Strobl
* @author Diego Krupitza
* @author Mark Paluch
+ * @see Specification
+ * @see org.springframework.data.jpa.domain.UpdateSpecification
+ * @see DeleteSpecification
+ * @see PredicateSpecification
*/
public interface JpaSpecificationExecutor {
+ /**
+ * Returns a single entity matching the given {@link PredicateSpecification} or {@link Optional#empty()} if none
+ * found.
+ *
+ * @param spec must not be {@literal null}.
+ * @return never {@literal null}.
+ * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one entity found.
+ * @see Specification#unrestricted()
+ */
+ default Optional findOne(PredicateSpecification spec) {
+ return findOne(Specification.where(spec));
+ }
+
/**
* Returns a single entity matching the given {@link Specification} or {@link Optional#empty()} if none found.
*
* @param spec must not be {@literal null}.
* @return never {@literal null}.
* @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one entity found.
+ * @see Specification#unrestricted()
*/
Optional findOne(Specification spec);
+ /**
+ * Returns all entities matching the given {@link PredicateSpecification}.
+ *
+ * @param spec must not be {@literal null}.
+ * @return never {@literal null}.
+ * @see Specification#unrestricted()
+ */
+ default List findAll(PredicateSpecification spec) {
+ return findAll(Specification.where(spec));
+ }
+
/**
* Returns all entities matching the given {@link Specification}.
- *
- * If no {@link Specification} is given all entities matching {@code } will be selected.
*
- * @param spec can be {@literal null}.
+ * @param spec must not be {@literal null}.
* @return never {@literal null}.
+ * @see Specification#unrestricted()
*/
- List findAll(@Nullable Specification spec);
+ List findAll(Specification spec);
/**
* Returns a {@link Page} of entities matching the given {@link Specification}.
- *
- * If no {@link Specification} is given all entities matching {@code } will be selected.
*
- * @param spec can be {@literal null}.
+ * @param spec must not be {@literal null}.
* @param pageable must not be {@literal null}.
* @return never {@literal null}.
+ * @see Specification#unrestricted()
*/
- Page findAll(@Nullable Specification spec, Pageable pageable);
+ Page findAll(Specification spec, Pageable pageable);
/**
* Returns all entities matching the given {@link Specification} and {@link Sort}.
- *
- * If no {@link Specification} is given all entities matching {@code } will be selected.
*
- * @param spec can be {@literal null}.
+ * @param spec must not be {@literal null}.
* @param sort must not be {@literal null}.
* @return never {@literal null}.
+ * @see Specification#unrestricted()
*/
- List findAll(@Nullable Specification spec, Sort sort);
+ List findAll(Specification spec, Sort sort);
+
+ /**
+ * Returns the number of instances that the given {@link PredicateSpecification} will return.
+ *
+ * @param spec the {@link PredicateSpecification} to count instances for, must not be {@literal null}.
+ * @return the number of instances.
+ * @see Specification#unrestricted()
+ */
+ default long count(PredicateSpecification spec) {
+ return count(Specification.where(spec));
+ }
/**
* Returns the number of instances that the given {@link Specification} will return.
- *
- * If no {@link Specification} is given all entities matching {@code } will be counted.
*
* @param spec the {@link Specification} to count instances for, must not be {@literal null}.
* @return the number of instances.
+ * @see Specification#unrestricted()
+ */
+ long count(Specification spec);
+
+ /**
+ * Checks whether the data store contains elements that match the given {@link PredicateSpecification}.
+ *
+ * @param spec the {@link PredicateSpecification} to use for the existence check, must not be {@literal null}.
+ * @return {@code true} if the data store contains elements that match the given {@link PredicateSpecification}
+ * otherwise {@code false}.
+ * @see Specification#unrestricted()
*/
- long count(@Nullable Specification spec);
+ default boolean exists(PredicateSpecification spec) {
+ return exists(Specification.where(spec));
+ }
/**
* Checks whether the data store contains elements that match the given {@link Specification}.
*
- * @param spec the {@link Specification} to use for the existence check, ust not be {@literal null}.
+ * @param spec the {@link Specification} to use for the existence check, must not be {@literal null}.
* @return {@code true} if the data store contains elements that match the given {@link Specification} otherwise
* {@code false}.
+ * @see Specification#unrestricted()
*/
boolean exists(Specification spec);
/**
- * Deletes by the {@link Specification} and returns the number of rows deleted.
+ * Updates entities by the {@link UpdateSpecification} and returns the number of rows updated.
+ *
+ * This method uses {@link jakarta.persistence.criteria.CriteriaUpdate Criteria API bulk update} that maps directly to
+ * database update operations. The persistence context is not synchronized with the result of the bulk update.
+ *
+ * @param spec the {@link UpdateSpecification} to use for the update query must not be {@literal null}.
+ * @return the number of entities deleted.
+ * @since xxx
+ */
+ long update(UpdateSpecification spec);
+
+ /**
+ * Deletes by the {@link PredicateSpecification} and returns the number of rows deleted.
*
* This method uses {@link jakarta.persistence.criteria.CriteriaDelete Criteria API bulk delete} that maps directly to
* database delete operations. The persistence context is not synchronized with the result of the bulk delete.
+ *
+ * @param spec the {@link PredicateSpecification} to use for the delete query, must not be {@literal null}.
+ * @return the number of entities deleted.
+ * @since 3.0
+ * @see PredicateSpecification#unrestricted()
+ */
+ default long delete(PredicateSpecification spec) {
+ return delete(DeleteSpecification.where(spec));
+ }
+
+ /**
+ * Deletes by the {@link UpdateSpecification} and returns the number of rows deleted.
*
- * Please note that {@link jakarta.persistence.criteria.CriteriaQuery} in,
- * {@link Specification#toPredicate(Root, CriteriaQuery, CriteriaBuilder)} will be {@literal null} because
- * {@link jakarta.persistence.criteria.CriteriaBuilder#createCriteriaDelete(Class)} does not implement
- * {@code CriteriaQuery}.
- *
- * If no {@link Specification} is given all entities matching {@code } will be deleted.
+ * This method uses {@link jakarta.persistence.criteria.CriteriaDelete Criteria API bulk delete} that maps directly to
+ * database delete operations. The persistence context is not synchronized with the result of the bulk delete.
*
- * @param spec the {@link Specification} to use for the existence check, can not be {@literal null}.
+ * @param spec the {@link UpdateSpecification} to use for the delete query must not be {@literal null}.
* @return the number of entities deleted.
* @since 3.0
+ * @see DeleteSpecification#unrestricted()
+ */
+ long delete(DeleteSpecification spec);
+
+ /**
+ * Returns entities matching the given {@link Specification} applying the {@code queryFunction} that defines the query
+ * and its result type.
+ *
+ * @param spec must not be null.
+ * @param queryFunction the query function defining projection, sorting, and the result type
+ * @return all entities matching the given Example.
+ * @since xxx
*/
- long delete(@Nullable Specification spec);
+ default R findBy(PredicateSpecification spec,
+ Function, R> queryFunction) {
+ return findBy(Specification.where(spec), queryFunction);
+ }
/**
* Returns entities matching the given {@link Specification} applying the {@code queryFunction} that defines the query
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java
index a5609c89b4..ff3d19a20f 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java
@@ -25,6 +25,7 @@
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaDelete;
import jakarta.persistence.criteria.CriteriaQuery;
+import jakarta.persistence.criteria.CriteriaUpdate;
import jakarta.persistence.criteria.ParameterExpression;
import jakarta.persistence.criteria.Path;
import jakarta.persistence.criteria.Predicate;
@@ -49,7 +50,9 @@
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.convert.QueryByExamplePredicateBuilder;
+import org.springframework.data.jpa.domain.DeleteSpecification;
import org.springframework.data.jpa.domain.Specification;
+import org.springframework.data.jpa.domain.UpdateSpecification;
import org.springframework.data.jpa.provider.PersistenceProvider;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.query.EscapeCharacter;
@@ -250,7 +253,7 @@ public void deleteAllByIdInBatch(Iterable ids) {
/*
* Some JPA providers require {@code ids} to be a {@link Collection} so we must convert if it's not already.
*/
- Collection idCollection = toCollection(ids);
+ Collection