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 idCollection = toCollection(ids); query.setParameter("ids", idCollection); applyQueryHints(query); @@ -389,7 +392,7 @@ public boolean existsById(ID id) { @Override public List findAll() { - return getQuery(null, Sort.unsorted()).getResultList(); + return getQuery(Specification.unrestricted(), Sort.unsorted()).getResultList(); } @Override @@ -422,12 +425,12 @@ public List findAllById(Iterable ids) { @Override public List findAll(Sort sort) { - return getQuery(null, sort).getResultList(); + return getQuery(Specification.unrestricted(), sort).getResultList(); } @Override public Page findAll(Pageable pageable) { - return findAll((Specification) null, pageable); + return findAll(Specification.unrestricted(), pageable); } @Override @@ -441,7 +444,7 @@ public List findAll(Specification spec) { } @Override - public Page findAll(@Nullable Specification spec, Pageable pageable) { + public Page findAll(Specification spec, Pageable pageable) { TypedQuery query = getQuery(spec, pageable); return pageable.isUnpaged() ? new PageImpl<>(query.getResultList()) @@ -449,13 +452,15 @@ public Page findAll(@Nullable Specification spec, Pageable pageable) { } @Override - public List findAll(@Nullable Specification spec, Sort sort) { + public List findAll(Specification spec, Sort sort) { return getQuery(spec, sort).getResultList(); } @Override public boolean exists(Specification spec) { + Assert.notNull(spec, "Specification must not be null"); + CriteriaQuery cq = this.entityManager.getCriteriaBuilder() // .createQuery(Integer.class) // .select(this.entityManager.getCriteriaBuilder().literal(1)); @@ -468,21 +473,20 @@ public boolean exists(Specification spec) { @Override @Transactional - public long delete(@Nullable Specification spec) { + public long update(UpdateSpecification spec) { - CriteriaBuilder builder = this.entityManager.getCriteriaBuilder(); - CriteriaDelete delete = builder.createCriteriaDelete(getDomainClass()); + Assert.notNull(spec, "Specification must not be null"); - if (spec != null) { - Predicate predicate = spec.toPredicate(delete.from(getDomainClass()), builder.createQuery(getDomainClass()), - builder); + return getUpdate(spec, getDomainClass()).executeUpdate(); + } - if (predicate != null) { - delete.where(predicate); - } - } + @Override + @Transactional + public long delete(DeleteSpecification spec) { - return this.entityManager.createQuery(delete).executeUpdate(); + Assert.notNull(spec, "Specification must not be null"); + + return getDelete(spec, getDomainClass()).executeUpdate(); } @Override @@ -494,6 +498,7 @@ public R findBy(Specification spec, Function R doFindBy(Specification spec, Class domainClass, Function, R> queryFunction) { @@ -582,6 +587,7 @@ public Page findAll(Example example, Pageable pageable) { } @Override + @SuppressWarnings("unchecked") public R findBy(Example example, Function, R> queryFunction) { Assert.notNull(example, EXAMPLE_MUST_NOT_BE_NULL); @@ -604,7 +610,7 @@ public long count() { } @Override - public long count(@Nullable Specification spec) { + public long count(Specification spec) { return executeCountQuery(getCountQuery(spec, getDomainClass())); } @@ -673,7 +679,7 @@ public void flush() { * @deprecated use {@link #readPage(TypedQuery, Class, Pageable, Specification)} instead */ @Deprecated - protected Page readPage(TypedQuery query, Pageable pageable, @Nullable Specification spec) { + protected Page readPage(TypedQuery query, Pageable pageable, Specification spec) { return readPage(query, getDomainClass(), pageable, spec); } @@ -683,11 +689,13 @@ protected Page readPage(TypedQuery query, Pageable pageable, @Nullable Spe * * @param query must not be {@literal null}. * @param domainClass must not be {@literal null}. - * @param spec can be {@literal null}. + * @param spec must not be {@literal null}. * @param pageable can be {@literal null}. */ protected Page readPage(TypedQuery query, final Class domainClass, Pageable pageable, - @Nullable Specification spec) { + Specification spec) { + + Assert.notNull(spec, "Specification must not be null"); if (pageable.isPaged()) { query.setFirstResult(PageableUtils.getOffsetAsInteger(pageable)); @@ -701,10 +709,10 @@ protected Page readPage(TypedQuery query, final Class dom /** * Creates a new {@link TypedQuery} from the given {@link Specification}. * - * @param spec can be {@literal null}. + * @param spec must not be {@literal null}. * @param pageable must not be {@literal null}. */ - protected TypedQuery getQuery(@Nullable Specification spec, Pageable pageable) { + protected TypedQuery getQuery(Specification spec, Pageable pageable) { return getQuery(spec, getDomainClass(), pageable.getSort()); } @@ -712,12 +720,11 @@ protected TypedQuery getQuery(@Nullable Specification spec, Pageable pagea /** * Creates a new {@link TypedQuery} from the given {@link Specification}. * - * @param spec can be {@literal null}. + * @param spec must not be {@literal null}. * @param domainClass must not be {@literal null}. * @param pageable must not be {@literal null}. */ - protected TypedQuery getQuery(@Nullable Specification spec, Class domainClass, - Pageable pageable) { + protected TypedQuery getQuery(Specification spec, Class domainClass, Pageable pageable) { return getQuery(spec, domainClass, pageable.getSort()); } @@ -725,21 +732,23 @@ protected TypedQuery getQuery(@Nullable Specification spec, /** * Creates a {@link TypedQuery} for the given {@link Specification} and {@link Sort}. * - * @param spec can be {@literal null}. + * @param spec must not be {@literal null}. * @param sort must not be {@literal null}. */ - protected TypedQuery getQuery(@Nullable Specification spec, Sort sort) { + protected TypedQuery getQuery(Specification spec, Sort sort) { return getQuery(spec, getDomainClass(), sort); } /** * Creates a {@link TypedQuery} for the given {@link Specification} and {@link Sort}. * - * @param spec can be {@literal null}. + * @param spec must not be {@literal null}. * @param domainClass must not be {@literal null}. * @param sort must not be {@literal null}. */ - protected TypedQuery getQuery(@Nullable Specification spec, Class domainClass, Sort sort) { + protected TypedQuery getQuery(Specification spec, Class domainClass, Sort sort) { + + Assert.notNull(spec, "Specification must not be null"); CriteriaBuilder builder = entityManager.getCriteriaBuilder(); CriteriaQuery query = builder.createQuery(domainClass); @@ -754,24 +763,62 @@ protected TypedQuery getQuery(@Nullable Specification spec, return applyRepositoryMethodMetadata(entityManager.createQuery(query)); } + /** + * Creates a {@link Query} for the given {@link UpdateSpecification}. + * + * @param spec must not be {@literal null}. + * @param domainClass must not be {@literal null}. + */ + protected Query getUpdate(UpdateSpecification spec, Class domainClass) { + + Assert.notNull(spec, "Specification must not be null"); + + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + CriteriaUpdate query = builder.createCriteriaUpdate(domainClass); + + applySpecificationToCriteria(spec, domainClass, query); + + return applyRepositoryMethodMetadata(entityManager.createQuery(query)); + } + + /** + * Creates a {@link Query} for the given {@link DeleteSpecification}. + * + * @param spec must not be {@literal null}. + * @param domainClass must not be {@literal null}. + */ + protected Query getDelete(DeleteSpecification spec, Class domainClass) { + + Assert.notNull(spec, "Specification must not be null"); + + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + CriteriaDelete query = builder.createCriteriaDelete(domainClass); + + applySpecificationToCriteria(spec, domainClass, query); + + return applyRepositoryMethodMetadata(entityManager.createQuery(query)); + } + /** * Creates a new count query for the given {@link Specification}. * - * @param spec can be {@literal null}. + * @param spec must not be {@literal null}. * @deprecated override {@link #getCountQuery(Specification, Class)} instead */ @Deprecated - protected TypedQuery getCountQuery(@Nullable Specification spec) { + protected TypedQuery getCountQuery(Specification spec) { return getCountQuery(spec, getDomainClass()); } /** * Creates a new count query for the given {@link Specification}. * - * @param spec can be {@literal null}. + * @param spec must not be {@literal null}. * @param domainClass must not be {@literal null}. */ - protected TypedQuery getCountQuery(@Nullable Specification spec, Class domainClass) { + protected TypedQuery getCountQuery(Specification spec, Class domainClass) { + + Assert.notNull(spec, "Specification must not be null"); CriteriaBuilder builder = entityManager.getCriteriaBuilder(); CriteriaQuery query = builder.createQuery(Long.class); @@ -805,33 +852,45 @@ protected QueryHints getQueryHintsForCount() { return metadata == null ? NoHints.INSTANCE : DefaultQueryHints.of(entityInformation, metadata).forCounts(); } - /** - * Applies the given {@link Specification} to the given {@link CriteriaQuery}. - * - * @param spec can be {@literal null}. - * @param domainClass must not be {@literal null}. - * @param query must not be {@literal null}. - */ - private Root applySpecificationToCriteria(@Nullable Specification spec, Class domainClass, + private Root applySpecificationToCriteria(Specification spec, Class domainClass, CriteriaQuery query) { - Assert.notNull(domainClass, "Domain class must not be null"); - Assert.notNull(query, "CriteriaQuery must not be null"); - Root root = query.from(domainClass); - if (spec == null) { - return root; + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + Predicate predicate = spec.toPredicate(root, query, builder); + + if (predicate != null) { + query.where(predicate); } + return root; + } + + private void applySpecificationToCriteria(UpdateSpecification spec, Class domainClass, + CriteriaUpdate query) { + + Root root = query.from(domainClass); + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); Predicate predicate = spec.toPredicate(root, query, builder); if (predicate != null) { query.where(predicate); } + } - return root; + private void applySpecificationToCriteria(DeleteSpecification spec, Class domainClass, + CriteriaDelete query) { + + Root root = query.from(domainClass); + + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + Predicate predicate = spec.toPredicate(root, query, builder); + + if (predicate != null) { + query.where(predicate); + } } private TypedQuery applyRepositoryMethodMetadata(TypedQuery query) { @@ -848,6 +907,20 @@ private TypedQuery applyRepositoryMethodMetadata(TypedQuery query) { return toReturn; } + private Query applyRepositoryMethodMetadata(Query query) { + + if (metadata == null) { + return query; + } + + LockModeType type = metadata.getLockModeType(); + Query toReturn = type == null ? query : query.setLockMode(type); + + applyQueryHints(toReturn); + + return toReturn; + } + private void applyQueryHints(Query query) { if (metadata == null) { @@ -895,7 +968,7 @@ private Map getHints() { private void applyComment(CrudMethodMetadata metadata, BiConsumer consumer) { if (metadata.getComment() != null && provider.getCommentHintKey() != null) { - consumer.accept(provider.getCommentHintKey(), provider.getCommentHintValue(this.metadata.getComment())); + consumer.accept(provider.getCommentHintKey(), provider.getCommentHintValue(metadata.getComment())); } } @@ -943,7 +1016,7 @@ private static long executeCountQuery(TypedQuery query) { @SuppressWarnings("rawtypes") private static final class ByIdsSpecification implements Specification { - @Serial private static final long serialVersionUID = 1L; + private static final @Serial long serialVersionUID = 1L; private final JpaEntityInformation entityInformation; @@ -954,6 +1027,7 @@ private static final class ByIdsSpecification implements Specification { } @Override + @SuppressWarnings("unchecked") public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder cb) { Path path = root.get(entityInformation.getIdAttribute()); @@ -972,7 +1046,7 @@ public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuild */ private static class ExampleSpecification implements Specification { - @Serial private static final long serialVersionUID = 1L; + private static final @Serial long serialVersionUID = 1L; private final Example example; private final EscapeCharacter escapeCharacter; diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/DeleteSpecificationUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/DeleteSpecificationUnitTests.java new file mode 100644 index 0000000000..02e59fa2db --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/DeleteSpecificationUnitTests.java @@ -0,0 +1,170 @@ +/* + * 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 static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; +import static org.springframework.util.SerializationUtils.*; + +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 org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +/** + * Unit tests for {@link DeleteSpecification}. + * + * @author Mark Paluch + */ +@SuppressWarnings("serial") +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class DeleteSpecificationUnitTests implements Serializable { + + private DeleteSpecification spec; + @Mock(serializable = true) Root root; + @Mock(serializable = true) CriteriaDelete delete; + @Mock(serializable = true) CriteriaBuilder builder; + @Mock(serializable = true) Predicate predicate; + @Mock(serializable = true) Predicate another; + + @BeforeEach + void setUp() { + spec = (root, delete, cb) -> predicate; + } + + @Test // GH-3521 + void allReturnsEmptyPredicate() { + + DeleteSpecification specification = DeleteSpecification.unrestricted(); + + assertThat(specification).isNotNull(); + assertThat(specification.toPredicate(root, delete, builder)).isNull(); + } + + @Test // GH-3521 + void allOfCombinesPredicatesInOrder() { + + DeleteSpecification specification = DeleteSpecification.allOf(spec); + + assertThat(specification).isNotNull(); + assertThat(specification.toPredicate(root, delete, builder)).isSameAs(predicate); + } + + @Test // GH-3521 + void anyOfCombinesPredicatesInOrder() { + + DeleteSpecification specification = DeleteSpecification.allOf(spec); + + assertThat(specification).isNotNull(); + assertThat(specification.toPredicate(root, delete, builder)).isSameAs(predicate); + } + + @Test // GH-3521 + void emptyAllOfReturnsEmptySpecification() { + + DeleteSpecification specification = DeleteSpecification.allOf(); + + assertThat(specification).isNotNull(); + assertThat(specification.toPredicate(root, delete, builder)).isNull(); + } + + @Test // GH-3521 + void emptyAnyOfReturnsEmptySpecification() { + + DeleteSpecification specification = DeleteSpecification.anyOf(); + + assertThat(specification).isNotNull(); + assertThat(specification.toPredicate(root, delete, builder)).isNull(); + } + + @Test // GH-3521 + void specificationsShouldBeSerializable() { + + DeleteSpecification serializableSpec = new SerializableSpecification(); + DeleteSpecification specification = serializableSpec.and(serializableSpec); + + assertThat(specification).isNotNull(); + + @SuppressWarnings("unchecked") + DeleteSpecification transferredSpecification = (DeleteSpecification) deserialize( + serialize(specification)); + + assertThat(transferredSpecification).isNotNull(); + } + + @Test // GH-3521 + void complexSpecificationsShouldBeSerializable() { + + SerializableSpecification serializableSpec = new SerializableSpecification(); + DeleteSpecification specification = DeleteSpecification + .not(serializableSpec.and(serializableSpec).or(serializableSpec)); + + assertThat(specification).isNotNull(); + + @SuppressWarnings("unchecked") + DeleteSpecification transferredSpecification = (DeleteSpecification) deserialize( + serialize(specification)); + + assertThat(transferredSpecification).isNotNull(); + } + + @Test // GH-3521 + void andCombinesSpecificationsInOrder() { + + Predicate firstPredicate = mock(Predicate.class); + Predicate secondPredicate = mock(Predicate.class); + + DeleteSpecification first = ((root1, delete, criteriaBuilder) -> firstPredicate); + DeleteSpecification second = ((root1, delete, criteriaBuilder) -> secondPredicate); + + first.and(second).toPredicate(root, delete, builder); + + verify(builder).and(firstPredicate, secondPredicate); + } + + @Test // GH-3521 + void orCombinesSpecificationsInOrder() { + + Predicate firstPredicate = mock(Predicate.class); + Predicate secondPredicate = mock(Predicate.class); + + DeleteSpecification first = ((root1, delete, criteriaBuilder) -> firstPredicate); + DeleteSpecification second = ((root1, delete, criteriaBuilder) -> secondPredicate); + + first.or(second).toPredicate(root, delete, builder); + + verify(builder).or(firstPredicate, secondPredicate); + } + + static class SerializableSpecification implements Serializable, DeleteSpecification { + + @Override + public Predicate toPredicate(Root root, CriteriaDelete delete, CriteriaBuilder cb) { + return null; + } + } +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/PredicateSpecificationUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/PredicateSpecificationUnitTests.java new file mode 100644 index 0000000000..f0cd8ca085 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/PredicateSpecificationUnitTests.java @@ -0,0 +1,168 @@ +/* + * 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 static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; +import static org.springframework.util.SerializationUtils.*; + +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; + +import java.io.Serializable; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +/** + * Unit tests for {@link PredicateSpecification}. + * + * @author Mark Paluch + */ +@SuppressWarnings("serial") +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class PredicateSpecificationUnitTests implements Serializable { + + private PredicateSpecification spec; + @Mock(serializable = true) Root root; + @Mock(serializable = true) CriteriaBuilder builder; + @Mock(serializable = true) Predicate predicate; + @Mock(serializable = true) Predicate another; + + @BeforeEach + void setUp() { + spec = (root, cb) -> predicate; + } + + @Test // GH-3521 + void allReturnsEmptyPredicate() { + + PredicateSpecification specification = PredicateSpecification.unrestricted(); + + assertThat(specification).isNotNull(); + assertThat(specification.toPredicate(root, builder)).isNull(); + } + + @Test // GH-3521 + void allOfCombinesPredicatesInOrder() { + + PredicateSpecification specification = PredicateSpecification.allOf(spec); + + assertThat(specification).isNotNull(); + assertThat(specification.toPredicate(root, builder)).isSameAs(predicate); + } + + @Test // GH-3521 + void anyOfCombinesPredicatesInOrder() { + + PredicateSpecification specification = PredicateSpecification.allOf(spec); + + assertThat(specification).isNotNull(); + assertThat(specification.toPredicate(root, builder)).isSameAs(predicate); + } + + @Test // GH-3521 + void emptyAllOfReturnsEmptySpecification() { + + PredicateSpecification specification = PredicateSpecification.allOf(); + + assertThat(specification).isNotNull(); + assertThat(specification.toPredicate(root, builder)).isNull(); + } + + @Test // GH-3521 + void emptyAnyOfReturnsEmptySpecification() { + + PredicateSpecification specification = PredicateSpecification.anyOf(); + + assertThat(specification).isNotNull(); + assertThat(specification.toPredicate(root, builder)).isNull(); + } + + @Test // GH-3521 + void specificationsShouldBeSerializable() { + + PredicateSpecification serializableSpec = new SerializableSpecification(); + PredicateSpecification specification = serializableSpec.and(serializableSpec); + + assertThat(specification).isNotNull(); + + @SuppressWarnings("unchecked") + PredicateSpecification transferredSpecification = (PredicateSpecification) deserialize( + serialize(specification)); + + assertThat(transferredSpecification).isNotNull(); + } + + @Test // GH-3521 + void complexSpecificationsShouldBeSerializable() { + + SerializableSpecification serializableSpec = new SerializableSpecification(); + PredicateSpecification specification = PredicateSpecification + .not(serializableSpec.and(serializableSpec).or(serializableSpec)); + + assertThat(specification).isNotNull(); + + @SuppressWarnings("unchecked") + PredicateSpecification transferredSpecification = (PredicateSpecification) deserialize( + serialize(specification)); + + assertThat(transferredSpecification).isNotNull(); + } + + @Test // GH-3521 + void andCombinesSpecificationsInOrder() { + + Predicate firstPredicate = mock(Predicate.class); + Predicate secondPredicate = mock(Predicate.class); + + PredicateSpecification first = ((root1, criteriaBuilder) -> firstPredicate); + PredicateSpecification second = ((root1, criteriaBuilder) -> secondPredicate); + + first.and(second).toPredicate(root, builder); + + verify(builder).and(firstPredicate, secondPredicate); + } + + @Test // GH-3521 + void orCombinesSpecificationsInOrder() { + + Predicate firstPredicate = mock(Predicate.class); + Predicate secondPredicate = mock(Predicate.class); + + PredicateSpecification first = ((root1, criteriaBuilder) -> firstPredicate); + PredicateSpecification second = ((root1, criteriaBuilder) -> secondPredicate); + + first.or(second).toPredicate(root, builder); + + verify(builder).or(firstPredicate, secondPredicate); + } + + static class SerializableSpecification implements Serializable, PredicateSpecification { + + @Override + public Predicate toPredicate(Root root, CriteriaBuilder cb) { + return null; + } + } +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/SpecificationUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/SpecificationUnitTests.java index 96f193b425..c493cbf4a8 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/SpecificationUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/SpecificationUnitTests.java @@ -17,8 +17,6 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; -import static org.springframework.data.jpa.domain.Specification.*; -import static org.springframework.data.jpa.domain.Specification.not; import static org.springframework.util.SerializationUtils.*; import jakarta.persistence.criteria.CriteriaBuilder; @@ -64,81 +62,8 @@ void setUp() { spec = (root, query, cb) -> predicate; } - @Test // DATAJPA-300, DATAJPA-1170 - void createsSpecificationsFromNull() { - - Specification specification = where(null); - assertThat(specification).isNotNull(); - assertThat(specification.toPredicate(root, query, builder)).isNull(); - } - - @Test // DATAJPA-300, DATAJPA-1170 - void negatesNullSpecToNull() { - - Specification specification = not(null); - - assertThat(specification).isNotNull(); - assertThat(specification.toPredicate(root, query, builder)).isNull(); - } - - @Test // DATAJPA-300, DATAJPA-1170 - void andConcatenatesSpecToNullSpec() { - - Specification specification = where(null); - specification = specification.and(spec); - - assertThat(specification).isNotNull(); - assertThat(specification.toPredicate(root, query, builder)).isEqualTo(predicate); - } - - @Test // DATAJPA-300, DATAJPA-1170 - void andConcatenatesNullSpecToSpec() { - - Specification specification = spec.and(null); - - assertThat(specification).isNotNull(); - assertThat(specification.toPredicate(root, query, builder)).isEqualTo(predicate); - } - - @Test // DATAJPA-300, DATAJPA-1170 - void orConcatenatesSpecToNullSpec() { - - Specification specification = where(null); - specification = specification.or(spec); - - assertThat(specification).isNotNull(); - assertThat(specification.toPredicate(root, query, builder)).isEqualTo(predicate); - } - - @Test // DATAJPA-300, DATAJPA-1170 - void orConcatenatesNullSpecToSpec() { - - Specification specification = spec.or(null); - - assertThat(specification).isNotNull(); - assertThat(specification.toPredicate(root, query, builder)).isEqualTo(predicate); - } - - @Test // GH-1943 - public void allOfConcatenatesNull() { - - Specification specification = Specification.allOf(null, spec, null); - - assertThat(specification).isNotNull(); - assertThat(specification.toPredicate(root, query, builder)).isEqualTo(predicate); - } - - @Test // GH-1943 - public void anyOfConcatenatesNull() { - - Specification specification = Specification.anyOf(null, spec, null); - - assertThat(specification).isNotNull(); - assertThat(specification.toPredicate(root, query, builder)).isEqualTo(predicate); - } - @Test // GH-1943 - public void emptyAllOfReturnsEmptySpecification() { + void emptyAllOfReturnsEmptySpecification() { Specification specification = Specification.allOf(); @@ -147,7 +72,7 @@ public void emptyAllOfReturnsEmptySpecification() { } @Test // GH-1943 - public void emptyAnyOfReturnsEmptySpecification() { + void emptyAnyOfReturnsEmptySpecification() { Specification specification = Specification.anyOf(); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/UpdateSpecificationUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/UpdateSpecificationUnitTests.java new file mode 100644 index 0000000000..540cc91e40 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/UpdateSpecificationUnitTests.java @@ -0,0 +1,170 @@ +/* + * 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 static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; +import static org.springframework.util.SerializationUtils.*; + +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 org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +/** + * Unit tests for {@link UpdateSpecification}. + * + * @author Mark Paluch + */ +@SuppressWarnings("serial") +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class UpdateSpecificationUnitTests implements Serializable { + + private UpdateSpecification spec; + @Mock(serializable = true) Root root; + @Mock(serializable = true) CriteriaUpdate update; + @Mock(serializable = true) CriteriaBuilder builder; + @Mock(serializable = true) Predicate predicate; + @Mock(serializable = true) Predicate another; + + @BeforeEach + void setUp() { + spec = (root, update, cb) -> predicate; + } + + @Test // GH-3521 + void allReturnsEmptyPredicate() { + + UpdateSpecification specification = UpdateSpecification.unrestricted(); + + assertThat(specification).isNotNull(); + assertThat(specification.toPredicate(root, update, builder)).isNull(); + } + + @Test // GH-3521 + void allOfCombinesPredicatesInOrder() { + + UpdateSpecification specification = UpdateSpecification.allOf(spec); + + assertThat(specification).isNotNull(); + assertThat(specification.toPredicate(root, update, builder)).isSameAs(predicate); + } + + @Test // GH-3521 + void anyOfCombinesPredicatesInOrder() { + + UpdateSpecification specification = UpdateSpecification.allOf(spec); + + assertThat(specification).isNotNull(); + assertThat(specification.toPredicate(root, update, builder)).isSameAs(predicate); + } + + @Test // GH-3521 + void emptyAllOfReturnsEmptySpecification() { + + UpdateSpecification specification = UpdateSpecification.allOf(); + + assertThat(specification).isNotNull(); + assertThat(specification.toPredicate(root, update, builder)).isNull(); + } + + @Test // GH-3521 + void emptyAnyOfReturnsEmptySpecification() { + + UpdateSpecification specification = UpdateSpecification.anyOf(); + + assertThat(specification).isNotNull(); + assertThat(specification.toPredicate(root, update, builder)).isNull(); + } + + @Test // GH-3521 + void specificationsShouldBeSerializable() { + + UpdateSpecification serializableSpec = new SerializableSpecification(); + UpdateSpecification specification = serializableSpec.and(serializableSpec); + + assertThat(specification).isNotNull(); + + @SuppressWarnings("unchecked") + UpdateSpecification transferredSpecification = (UpdateSpecification) deserialize( + serialize(specification)); + + assertThat(transferredSpecification).isNotNull(); + } + + @Test // GH-3521 + void complexSpecificationsShouldBeSerializable() { + + SerializableSpecification serializableSpec = new SerializableSpecification(); + UpdateSpecification specification = UpdateSpecification + .not(serializableSpec.and(serializableSpec).or(serializableSpec)); + + assertThat(specification).isNotNull(); + + @SuppressWarnings("unchecked") + UpdateSpecification transferredSpecification = (UpdateSpecification) deserialize( + serialize(specification)); + + assertThat(transferredSpecification).isNotNull(); + } + + @Test // GH-3521 + void andCombinesSpecificationsInOrder() { + + Predicate firstPredicate = mock(Predicate.class); + Predicate secondPredicate = mock(Predicate.class); + + UpdateSpecification first = ((root1, update, criteriaBuilder) -> firstPredicate); + UpdateSpecification second = ((root1, update, criteriaBuilder) -> secondPredicate); + + first.and(second).toPredicate(root, update, builder); + + verify(builder).and(firstPredicate, secondPredicate); + } + + @Test // GH-3521 + void orCombinesSpecificationsInOrder() { + + Predicate firstPredicate = mock(Predicate.class); + Predicate secondPredicate = mock(Predicate.class); + + UpdateSpecification first = ((root1, update, criteriaBuilder) -> firstPredicate); + UpdateSpecification second = ((root1, update, criteriaBuilder) -> secondPredicate); + + first.or(second).toPredicate(root, update, builder); + + verify(builder).or(firstPredicate, secondPredicate); + } + + static class SerializableSpecification implements Serializable, UpdateSpecification { + + @Override + public Predicate toPredicate(Root root, CriteriaUpdate update, CriteriaBuilder cb) { + return null; + } + } +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/UserSpecifications.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/UserSpecifications.java index 304dcb5607..cbd8ffd410 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/UserSpecifications.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/UserSpecifications.java @@ -15,6 +15,7 @@ */ package org.springframework.data.jpa.domain.sample; +import org.springframework.data.jpa.domain.PredicateSpecification; import org.springframework.data.jpa.domain.Specification; /** @@ -25,24 +26,24 @@ */ public class UserSpecifications { - public static Specification userHasFirstname(final String firstname) { + public static PredicateSpecification userHasFirstname(final String firstname) { return simplePropertySpec("firstname", firstname); } - public static Specification userHasLastname(final String lastname) { + public static PredicateSpecification userHasLastname(final String lastname) { return simplePropertySpec("lastname", lastname); } - public static Specification userHasFirstnameLike(final String expression) { + public static PredicateSpecification userHasFirstnameLike(final String expression) { - return (root, query, cb) -> cb.like(root.get("firstname").as(String.class), String.format("%%%s%%", expression)); + return (root, cb) -> cb.like(root.get("firstname").as(String.class), String.format("%%%s%%", expression)); } - public static Specification userHasAgeLess(final Integer age) { + public static PredicateSpecification userHasAgeLess(final Integer age) { - return (root, query, cb) -> cb.lessThan(root.get("age").as(Integer.class), age); + return (root, cb) -> cb.lessThan(root.get("age").as(Integer.class), age); } public static Specification userHasLastnameLikeWithSort(final String expression) { @@ -55,8 +56,8 @@ public static Specification userHasLastnameLikeWithSort(final String expre }; } - private static Specification simplePropertySpec(final String property, final Object value) { + private static PredicateSpecification simplePropertySpec(final String property, final Object value) { - return (root, query, builder) -> builder.equal(root.get(property), value); + return (root, builder) -> builder.equal(root.get(property), value); } } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java index d3f7edecd1..6b314833a5 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java @@ -20,8 +20,6 @@ import static org.springframework.data.domain.Example.*; import static org.springframework.data.domain.ExampleMatcher.*; import static org.springframework.data.domain.Sort.Direction.*; -import static org.springframework.data.jpa.domain.Specification.*; -import static org.springframework.data.jpa.domain.Specification.not; import static org.springframework.data.jpa.domain.sample.UserSpecifications.*; import jakarta.persistence.EntityManager; @@ -61,7 +59,10 @@ import org.springframework.data.domain.*; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.domain.Sort.Order; +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.jpa.domain.sample.Address; import org.springframework.data.jpa.domain.sample.QUser; import org.springframework.data.jpa.domain.sample.Role; @@ -468,7 +469,7 @@ void testExecutionOfProjectingMethod() { void executesSpecificationCorrectly() { flushTestUsers(); - assertThat(repository.findAll(where(userHasFirstname("Oliver")))).hasSize(1); + assertThat(repository.findAll(Specification.where(userHasFirstname("Oliver")))).hasSize(1); } @Test @@ -498,11 +499,11 @@ void throwsExceptionForUnderSpecifiedSingleEntitySpecification() { void executesCombinedSpecificationsCorrectly() { flushTestUsers(); - Specification spec1 = userHasFirstname("Oliver").or(userHasLastname("Arrasz")); + PredicateSpecification spec1 = userHasFirstname("Oliver").or(userHasLastname("Arrasz")); List users1 = repository.findAll(spec1); assertThat(users1).hasSize(2); - Specification spec2 = Specification.anyOf( // + PredicateSpecification spec2 = PredicateSpecification.anyOf( // userHasFirstname("Oliver"), // userHasLastname("Arrasz")); List users2 = repository.findAll(spec2); @@ -515,7 +516,8 @@ void executesCombinedSpecificationsCorrectly() { void executesNegatingSpecificationCorrectly() { flushTestUsers(); - Specification spec = not(userHasFirstname("Oliver")).and(userHasLastname("Arrasz")); + PredicateSpecification spec = PredicateSpecification.not(userHasFirstname("Oliver")) + .and(userHasLastname("Arrasz")); assertThat(repository.findAll(spec)).containsOnly(secondUser); } @@ -524,18 +526,18 @@ void executesNegatingSpecificationCorrectly() { void executesCombinedSpecificationsWithPageableCorrectly() { flushTestUsers(); - Specification spec1 = userHasFirstname("Oliver").or(userHasLastname("Arrasz")); + PredicateSpecification spec1 = userHasFirstname("Oliver").or(userHasLastname("Arrasz")); - Page users1 = repository.findAll(spec1, PageRequest.of(0, 1)); + Page users1 = repository.findAll(Specification.where(spec1), PageRequest.of(0, 1)); assertThat(users1.getSize()).isOne(); assertThat(users1.hasPrevious()).isFalse(); assertThat(users1.getTotalElements()).isEqualTo(2L); - Specification spec2 = Specification.anyOf( // + PredicateSpecification spec2 = PredicateSpecification.anyOf( // userHasFirstname("Oliver"), // userHasLastname("Arrasz")); - Page users2 = repository.findAll(spec2, PageRequest.of(0, 1)); + Page users2 = repository.findAll(Specification.where(spec2), PageRequest.of(0, 1)); assertThat(users2.getSize()).isOne(); assertThat(users2.hasPrevious()).isFalse(); assertThat(users2.getTotalElements()).isEqualTo(2L); @@ -590,7 +592,7 @@ void executesSimpleNotCorrectly() { void returnsSameListIfNoSpecGiven() { flushTestUsers(); - assertSameElements(repository.findAll(), repository.findAll((Specification) null)); + assertSameElements(repository.findAll(), repository.findAll(PredicateSpecification.unrestricted())); } @Test @@ -606,15 +608,41 @@ void returnsSamePageIfNoSpecGiven() { Pageable pageable = PageRequest.of(0, 1); flushTestUsers(); - assertThat(repository.findAll((Specification) null, pageable)).isEqualTo(repository.findAll(pageable)); + assertThat(repository.findAll(Specification.unrestricted(), pageable)).isEqualTo(repository.findAll(pageable)); + } + + @Test // GH-3521 + void updateSpecificationUpdatesMarriedEntities() { + + flushTestUsers(); + + UpdateSpecification updateLastname = UpdateSpecification. update((root, update, criteriaBuilder) -> { + update.set("lastname", "Drotbohm"); + }).where(userHasFirstname("Oliver").and(userHasLastname("Gierke"))); + + long updated = repository.update(updateLastname); + + assertThat(updated).isOne(); + assertThat(repository.count(userHasFirstname("Oliver").and(userHasLastname("Gierke")))).isZero(); + assertThat(repository.count(userHasFirstname("Oliver").and(userHasLastname("Drotbohm")))).isOne(); + } + + @Test // GH-2796 + void predicateSpecificationRemovesAll() { + + flushTestUsers(); + + repository.delete(DeleteSpecification.unrestricted()); + + assertThat(repository.count()).isEqualTo(0L); } @Test // GH-2796 - void removesAllIfSpecificationIsNull() { + void deleteSpecificationRemovesAll() { flushTestUsers(); - repository.delete((Specification) null); + repository.delete(DeleteSpecification.unrestricted()); assertThat(repository.count()).isEqualTo(0L); } @@ -3272,8 +3300,8 @@ void existsWithSpec() { flushTestUsers(); - Specification minorSpec = userHasAgeLess(18); - Specification hundredYearsOld = userHasAgeLess(100); + PredicateSpecification minorSpec = userHasAgeLess(18); + PredicateSpecification hundredYearsOld = userHasAgeLess(100); assertThat(repository.exists(minorSpec)).isFalse(); assertThat(repository.exists(hundredYearsOld)).isTrue(); @@ -3298,7 +3326,7 @@ void deleteWithSpec() { flushTestUsers(); - Specification usersWithEInTheirName = userHasFirstnameLike("e"); + PredicateSpecification usersWithEInTheirName = userHasFirstnameLike("e"); long initialCount = repository.count(); assertThat(repository.delete(usersWithEInTheirName)).isEqualTo(3L); @@ -3445,16 +3473,16 @@ private Page executeSpecWithSort(Sort sort) { flushTestUsers(); - Specification spec1 = userHasFirstname("Oliver").or(userHasLastname("Matthews")); + PredicateSpecification spec1 = userHasFirstname("Oliver").or(userHasLastname("Matthews")); - Page result1 = repository.findAll(spec1, PageRequest.of(0, 1, sort)); + Page result1 = repository.findAll(Specification.where(spec1), PageRequest.of(0, 1, sort)); assertThat(result1.getTotalElements()).isEqualTo(2L); - Specification spec2 = Specification.anyOf( // + PredicateSpecification spec2 = PredicateSpecification.anyOf( // userHasFirstname("Oliver"), // userHasLastname("Matthews")); - Page result2 = repository.findAll(spec2, PageRequest.of(0, 1, sort)); + Page result2 = repository.findAll(Specification.where(spec2), PageRequest.of(0, 1, sort)); assertThat(result2.getTotalElements()).isEqualTo(2L); assertThat(result1).containsExactlyElementsOf(result2); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/SimpleJpaRepositoryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/SimpleJpaRepositoryUnitTests.java index 24e43d24cb..86a73b1f3c 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/SimpleJpaRepositoryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/SimpleJpaRepositoryUnitTests.java @@ -15,13 +15,9 @@ */ package org.springframework.data.jpa.repository.support; -import static java.util.Collections.singletonMap; -import static org.assertj.core.api.Assertions.assertThatNoException; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.springframework.data.jpa.domain.Specification.where; +import static java.util.Collections.*; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; import jakarta.persistence.EntityGraph; import jakarta.persistence.EntityManager; @@ -41,7 +37,9 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; + import org.springframework.data.domain.PageRequest; +import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.domain.sample.User; import org.springframework.data.jpa.repository.EntityGraph.EntityGraphType; import org.springframework.data.repository.CrudRepository; @@ -212,7 +210,7 @@ void applyQueryHintsToCountQueriesForSpecificationPageables() { when(query.getResultList()).thenReturn(Arrays.asList(new User(), new User())); - repo.findAll(where(null), PageRequest.of(2, 1)); + repo.findAll(Specification.unrestricted(), PageRequest.of(2, 1)); verify(metadata).getQueryHintsForCount(); }