From f122ea90c68c7d980777cd224fea04b955f03306 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 12 Aug 2024 14:30:06 +0200 Subject: [PATCH 1/7] Prepare issue branch. --- pom.xml | 2 +- spring-data-envers/pom.xml | 4 ++-- spring-data-jpa-distribution/pom.xml | 2 +- spring-data-jpa/pom.xml | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) 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 From 63962633497efdb728760e49ade00ca7fbe87ac6 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 13 Aug 2024 09:30:05 +0200 Subject: [PATCH 2/7] Explore refined Specification API. Introduce DeleteSpecification and UpdateSpecification. Add PredicateSpecification. Update SpecificationExecutor. --- .../data/jpa/domain/DeleteSpecification.java | 217 ++++++++++++ .../jpa/domain/PredicateSpecification.java | 174 ++++++++++ .../data/jpa/domain/Specification.java | 160 ++++++--- .../jpa/domain/SpecificationComposition.java | 77 ++++- .../data/jpa/domain/UpdateSpecification.java | 314 ++++++++++++++++++ .../repository/JpaSpecificationExecutor.java | 142 ++++++-- .../support/SimpleJpaRepository.java | 136 ++++++-- .../domain/DeleteSpecificationUnitTests.java | 170 ++++++++++ .../PredicateSpecificationUnitTests.java | 168 ++++++++++ .../jpa/domain/SpecificationUnitTests.java | 79 +---- .../domain/UpdateSpecificationUnitTests.java | 170 ++++++++++ .../jpa/domain/sample/UserSpecifications.java | 17 +- .../jpa/repository/UserRepositoryTests.java | 70 ++-- .../support/SimpleJpaRepositoryUnitTests.java | 14 +- 14 files changed, 1687 insertions(+), 221 deletions(-) create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/DeleteSpecification.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/PredicateSpecification.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/UpdateSpecification.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/DeleteSpecificationUnitTests.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/PredicateSpecificationUnitTests.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/UpdateSpecificationUnitTests.java 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..b3bfd93ae2 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/DeleteSpecification.java @@ -0,0 +1,217 @@ +/* + * 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.Serial; +import java.io.Serializable; +import java.util.Arrays; +import java.util.stream.StreamSupport; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Specification in the sense of Domain Driven Design to handle Criteria Deletes. + * + * @author Mark Paluch + * @since xxx + */ +@FunctionalInterface +public interface DeleteSpecification extends Serializable { + + @Serial long serialVersionUID = 1L; + + /** + * 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 all() { + 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. + */ + 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. + */ + 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. + */ + 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. + */ + 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 not = spec.toPredicate(root, delete, builder); + return not != null ? builder.not(not) : null; + }; + } + + /** + * Applies an AND operation to all the given {@link DeleteSpecification}s. + * + * @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. + * + * @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.all(), DeleteSpecification::and); + } + + /** + * Applies an OR operation to all the given {@link DeleteSpecification}s. + * + * @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. + * + * @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.all(), 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..b3e52f4249 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/PredicateSpecification.java @@ -0,0 +1,174 @@ +/* + * 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.Serial; +import java.io.Serializable; +import java.util.Arrays; +import java.util.stream.StreamSupport; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Specification in the sense of Domain Driven Design. + * + * @author Mark Paluch + * @since xxx + */ +public interface PredicateSpecification extends Serializable { + + @Serial long serialVersionUID = 1L; + + /** + * 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 all() { + 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, "DeleteSpecification 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. + */ + 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. + */ + 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 not = spec.toPredicate(root, builder); + return not != null ? builder.not(not) : null; + }; + } + + /** + * Applies an AND operation to all the given {@link PredicateSpecification}s. + * + * @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. + * + * @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.all(), PredicateSpecification::and); + } + + /** + * Applies an OR operation to all the given {@link PredicateSpecification}s. + * + * @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. + * + * @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.all(), 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..4aca01862b 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,6 +17,7 @@ import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.CriteriaUpdate; import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; @@ -26,6 +27,7 @@ import java.util.stream.StreamSupport; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; /** * Specification in the sense of Domain Driven Design. @@ -39,86 +41,132 @@ * @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 all() { + return (root, query, builder) -> null; } /** * Simple static factory method to add some syntactic sugar around a {@link Specification}. * * @param the type of the {@link Root} the resulting {@literal Specification} operates on. - * @param spec can be {@literal null}. + * @param spec must not be {@literal null}. * @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(Specification spec) { + + Assert.notNull(spec, "Specification must not be null"); + + return spec; + } + + /** + * 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 the {@link PredicateSpecification} to wrap. + * @return guaranteed to be not {@literal null}. + */ + static Specification where(PredicateSpecification spec) { + + Assert.notNull(spec, "PredicateSpecification must not be null"); + + return where((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) { + 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 + */ + 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) { + 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); + 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 not = spec.toPredicate(root, query, builder); + return not != null ? builder.not(not) : null; + }; } /** + * Applies an AND operation to all the given {@link Specification}s. + * + * @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 +176,26 @@ 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. * - * @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.all(), Specification::and); } /** + * Applies an OR operation to all the given {@link Specification}s. + * + * @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 +203,32 @@ 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. + * + * @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.all(), 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..8e217fc0f4 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/UpdateSpecification.java @@ -0,0 +1,314 @@ +/* + * 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.Serial; +import java.io.Serializable; +import java.util.Arrays; +import java.util.stream.StreamSupport; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Specification in the sense of Domain Driven Design to handle Criteria Updates. + * + * @author Mark Paluch + * @since xxx + */ +@FunctionalInterface +public interface UpdateSpecification extends Serializable { + + @Serial long serialVersionUID = 1L; + + /** + * Simple static factory method to create a specification deleting all objects. + * + * @param the type of the {@link Root} the resulting {@literal UpdateSpecification} operates on. + * @return guaranteed to be not {@literal null}. + */ + static UpdateSpecification all() { + return (root, query, builder) -> null; + } + + /** + * Simple static factory method to add some syntactic sugar around a {@literal UpdateSpecification}. 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 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 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. + */ + 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. + */ + 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. + */ + 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. + */ + 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 not = spec.toPredicate(root, update, builder); + return not != null ? builder.not(not) : null; + }; + } + + /** + * Applies an AND operation to all the given {@link UpdateSpecification}s. + * + * @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. + * + * @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.all(), UpdateSpecification::and); + } + + /** + * Applies an OR operation to all the given {@link UpdateSpecification}s. + * + * @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. + * + * @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.all(), 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. + */ + 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. + */ + 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. + */ + 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..1965aaee7b 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#all() + */ + 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#all() */ 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#all() + */ + 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#all() */ - 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#all() */ - 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#all() */ - 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#all() + */ + 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#all() + */ + 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#all() */ - 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#all() */ 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#all() + */ + 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#all() + */ + 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..e29a736bed 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; @@ -389,7 +392,7 @@ public boolean existsById(ID id) { @Override public List findAll() { - return getQuery(null, Sort.unsorted()).getResultList(); + return getQuery(Specification.all(), 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.all(), sort).getResultList(); } @Override public Page findAll(Pageable pageable) { - return findAll((Specification) null, pageable); + return findAll(Specification.all(), 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) { + + Assert.notNull(spec, "Specification must not be null"); - return this.entityManager.createQuery(delete).executeUpdate(); + return getDelete(spec, getDomainClass()).executeUpdate(); } @Override @@ -725,21 +729,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,6 +760,42 @@ 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}. * @@ -805,33 +847,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 +902,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) { 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..79e531ad7f --- /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.all(); + + 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..f2f8a83a43 --- /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.all(); + + 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..f66bba7d73 --- /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.all(); + + 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..e273400b57 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.all())); } @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.all(), 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.all()); + + assertThat(repository.count()).isEqualTo(0L); } @Test // GH-2796 - void removesAllIfSpecificationIsNull() { + void deleteSpecificationRemovesAll() { flushTestUsers(); - repository.delete((Specification) null); + repository.delete(DeleteSpecification.all()); 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..85bd6b509f 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.all(), PageRequest.of(2, 1)); verify(metadata).getQueryHintsForCount(); } From c128fea82b98c7789d63dd91e12fec0926973e61 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 19 Aug 2024 15:41:07 +0200 Subject: [PATCH 3/7] Remove Specification.where method in favour of all() Gentle reminder to deprecate where before we remove it here --- .../data/jpa/domain/Specification.java | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) 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 4aca01862b..26cf856603 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 @@ -56,21 +56,6 @@ static Specification all() { return (root, query, builder) -> null; } - /** - * Simple static factory method to add some syntactic sugar around a {@link Specification}. - * - * @param the type of the {@link Root} the resulting {@literal Specification} operates on. - * @param spec must not be {@literal null}. - * @return guaranteed to be not {@literal null}. - * @since 2.0 - */ - static Specification where(Specification spec) { - - Assert.notNull(spec, "Specification must not be null"); - - return spec; - } - /** * Simple static factory method to add some syntactic sugar translating {@link PredicateSpecification} to * {@link Specification}. @@ -83,7 +68,7 @@ static Specification where(PredicateSpecification spec) { Assert.notNull(spec, "PredicateSpecification must not be null"); - return where((root, update, criteriaBuilder) -> spec.toPredicate(root, criteriaBuilder)); + return (root, update, criteriaBuilder) -> spec.toPredicate(root, criteriaBuilder); } /** From 6eb31e8384bd2299bf9704ddfd060e9b45d032e4 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 20 Aug 2024 10:44:37 +0200 Subject: [PATCH 4/7] Remove serialVersionUID --- .../springframework/data/jpa/domain/DeleteSpecification.java | 3 --- .../data/jpa/domain/PredicateSpecification.java | 3 --- .../org/springframework/data/jpa/domain/Specification.java | 3 --- .../springframework/data/jpa/domain/UpdateSpecification.java | 3 --- 4 files changed, 12 deletions(-) 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 index b3bfd93ae2..738ad212ce 100644 --- 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 @@ -20,7 +20,6 @@ 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; @@ -37,8 +36,6 @@ @FunctionalInterface public interface DeleteSpecification extends Serializable { - @Serial long serialVersionUID = 1L; - /** * Simple static factory method to create a specification deleting all objects. * 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 index b3e52f4249..49ff92c5ba 100644 --- 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 @@ -19,7 +19,6 @@ 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; @@ -35,8 +34,6 @@ */ public interface PredicateSpecification extends Serializable { - @Serial long serialVersionUID = 1L; - /** * Simple static factory method to create a specification matching all objects. * 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 26cf856603..73e45e308f 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 @@ -21,7 +21,6 @@ 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; @@ -44,8 +43,6 @@ @FunctionalInterface public interface Specification extends Serializable { - @Serial long serialVersionUID = 1L; - /** * Simple static factory method to create a specification matching all objects. * 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 index 8e217fc0f4..2872b0ab0f 100644 --- 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 @@ -20,7 +20,6 @@ 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; @@ -37,8 +36,6 @@ @FunctionalInterface public interface UpdateSpecification extends Serializable { - @Serial long serialVersionUID = 1L; - /** * Simple static factory method to create a specification deleting all objects. * From bffccbe23106c65f47e5b3843a35ca0f9f890e52 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 20 Aug 2024 14:47:34 +0200 Subject: [PATCH 5/7] Polishing. Revise nullability requirements around non-nullable specifications. --- .../support/SimpleJpaRepository.java | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) 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 e29a736bed..1df9c1b198 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 @@ -498,6 +498,7 @@ public R findBy(Specification spec, Function R doFindBy(Specification spec, Class domainClass, Function, R> queryFunction) { @@ -586,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); @@ -608,7 +610,7 @@ public long count() { } @Override - public long count(@Nullable Specification spec) { + public long count(Specification spec) { return executeCountQuery(getCountQuery(spec, getDomainClass())); } @@ -677,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); } @@ -687,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)); @@ -705,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()); } @@ -716,11 +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, + protected TypedQuery getQuery(Specification spec, Class domainClass, Pageable pageable) { return getQuery(spec, domainClass, pageable.getSort()); @@ -799,21 +803,23 @@ protected Query getDelete(DeleteSpecification spec, Class domainClass) /** * 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); @@ -963,7 +969,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())); } } From e88a8954c04c09e4150be4bac3a0d4d67ba33303 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 7 Nov 2024 15:56:30 +0100 Subject: [PATCH 6/7] Add Contract annotations. --- .../data/jpa/domain/DeleteSpecification.java | 12 +++++++++++- .../jpa/domain/PredicateSpecification.java | 10 ++++++++-- .../data/jpa/domain/Specification.java | 10 ++++++++++ .../data/jpa/domain/UpdateSpecification.java | 19 ++++++++++++++++++- .../support/SimpleJpaRepository.java | 10 +++++----- 5 files changed, 52 insertions(+), 9 deletions(-) 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 index 738ad212ce..310ed0c6da 100644 --- 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 @@ -24,6 +24,8 @@ 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; @@ -31,7 +33,7 @@ * Specification in the sense of Domain Driven Design to handle Criteria Deletes. * * @author Mark Paluch - * @since xxx + * @since 4.0 */ @FunctionalInterface public interface DeleteSpecification extends Serializable { @@ -81,6 +83,8 @@ static DeleteSpecification where(PredicateSpecification spec) { * @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"); @@ -94,6 +98,8 @@ default DeleteSpecification and(DeleteSpecification other) { * @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"); @@ -107,6 +113,8 @@ default DeleteSpecification and(PredicateSpecification other) { * @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"); @@ -120,6 +128,8 @@ default DeleteSpecification or(DeleteSpecification other) { * @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"); 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 index 49ff92c5ba..f237715bc0 100644 --- 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 @@ -23,6 +23,8 @@ 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; @@ -30,7 +32,7 @@ * Specification in the sense of Domain Driven Design. * * @author Mark Paluch - * @since xxx + * @since 4.0 */ public interface PredicateSpecification extends Serializable { @@ -54,7 +56,7 @@ static PredicateSpecification all() { */ static PredicateSpecification where(PredicateSpecification spec) { - Assert.notNull(spec, "DeleteSpecification must not be null"); + Assert.notNull(spec, "PredicateSpecification must not be null"); return spec; } @@ -65,6 +67,8 @@ static PredicateSpecification where(PredicateSpecification spec) { * @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"); @@ -78,6 +82,8 @@ default PredicateSpecification and(PredicateSpecification other) { * @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"); 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 73e45e308f..975d52d6ec 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 @@ -25,6 +25,8 @@ 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; @@ -75,6 +77,8 @@ static Specification where(PredicateSpecification spec) { * @return the conjunction of the specifications. * @since 2.0 */ + @Contract("_ -> new") + @CheckReturnValue default Specification and(Specification other) { Assert.notNull(other, "Other specification must not be null"); @@ -89,6 +93,8 @@ default Specification and(Specification other) { * @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"); @@ -103,6 +109,8 @@ default Specification and(PredicateSpecification other) { * @return the disjunction of the specifications * @since 2.0 */ + @Contract("_ -> new") + @CheckReturnValue default Specification or(Specification other) { Assert.notNull(other, "Other specification must not be null"); @@ -117,6 +125,8 @@ default Specification or(Specification other) { * @return the disjunction of the specifications * @since 2.0 */ + @Contract("_ -> new") + @CheckReturnValue default Specification or(PredicateSpecification other) { Assert.notNull(other, "Other specification must not be null"); 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 index 2872b0ab0f..7667faa9c4 100644 --- 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 @@ -24,6 +24,8 @@ 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; @@ -31,7 +33,7 @@ * Specification in the sense of Domain Driven Design to handle Criteria Updates. * * @author Mark Paluch - * @since xxx + * @since 4.0 */ @FunctionalInterface public interface UpdateSpecification extends Serializable { @@ -103,6 +105,8 @@ static UpdateSpecification where(PredicateSpecification spec) { * @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"); @@ -116,6 +120,8 @@ default UpdateSpecification and(UpdateSpecification other) { * @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"); @@ -129,6 +135,8 @@ default UpdateSpecification and(PredicateSpecification other) { * @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"); @@ -142,6 +150,8 @@ default UpdateSpecification or(UpdateSpecification other) { * @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"); @@ -256,6 +266,8 @@ interface UpdateOperation { * @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"); @@ -272,6 +284,8 @@ default UpdateOperation and(UpdateOperation other) { * @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"); @@ -288,6 +302,8 @@ default UpdateSpecification where(PredicateSpecification specification) { * @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"); @@ -306,6 +322,7 @@ default UpdateSpecification where(UpdateSpecification specification) { * @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/support/SimpleJpaRepository.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java index 1df9c1b198..3c137b4329 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 @@ -253,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); @@ -724,8 +724,7 @@ protected TypedQuery getQuery(Specification spec, Pageable pageable) { * @param domainClass must not be {@literal null}. * @param pageable must not be {@literal null}. */ - protected TypedQuery getQuery(Specification spec, Class domainClass, - Pageable pageable) { + protected TypedQuery getQuery(Specification spec, Class domainClass, Pageable pageable) { return getQuery(spec, domainClass, pageable.getSort()); } @@ -1017,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; + @Serial private static final @Serial long serialVersionUID = 1L; private final JpaEntityInformation entityInformation; @@ -1028,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()); @@ -1046,7 +1046,7 @@ public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuild */ private static class ExampleSpecification implements Specification { - @Serial private static final long serialVersionUID = 1L; + @Serial private static final @Serial long serialVersionUID = 1L; private final Example example; private final EscapeCharacter escapeCharacter; From 490eef0f954f85e708aa89e16031b18d91f662ec Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 8 Jan 2025 11:24:51 +0100 Subject: [PATCH 7/7] Address review findings. --- .../data/jpa/domain/DeleteSpecification.java | 28 ++++++++++----- .../jpa/domain/PredicateSpecification.java | 28 ++++++++++----- .../data/jpa/domain/Specification.java | 28 ++++++++++----- .../data/jpa/domain/UpdateSpecification.java | 36 ++++++++++++------- .../repository/JpaSpecificationExecutor.java | 24 ++++++------- .../support/SimpleJpaRepository.java | 10 +++--- .../domain/DeleteSpecificationUnitTests.java | 2 +- .../PredicateSpecificationUnitTests.java | 2 +- .../domain/UpdateSpecificationUnitTests.java | 2 +- .../jpa/repository/UserRepositoryTests.java | 8 ++--- .../support/SimpleJpaRepositoryUnitTests.java | 2 +- 11 files changed, 105 insertions(+), 65 deletions(-) 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 index 310ed0c6da..3337ae5fb1 100644 --- 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 @@ -31,6 +31,12 @@ /** * 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 @@ -44,7 +50,7 @@ public interface DeleteSpecification extends Serializable { * @param the type of the {@link Root} the resulting {@literal DeleteSpecification} operates on. * @return guaranteed to be not {@literal null}. */ - static DeleteSpecification all() { + static DeleteSpecification unrestricted() { return (root, query, builder) -> null; } @@ -150,13 +156,14 @@ static DeleteSpecification not(DeleteSpecification spec) { return (root, delete, builder) -> { - Predicate not = spec.toPredicate(root, delete, builder); - return not != null ? builder.not(not) : null; + 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. + * 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. @@ -169,7 +176,8 @@ static DeleteSpecification allOf(DeleteSpecification... specifications } /** - * Applies an AND operation to all the given {@link DeleteSpecification}s. + * 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. @@ -179,11 +187,12 @@ static DeleteSpecification allOf(DeleteSpecification... specifications static DeleteSpecification allOf(Iterable> specifications) { return StreamSupport.stream(specifications.spliterator(), false) // - .reduce(DeleteSpecification.all(), DeleteSpecification::and); + .reduce(DeleteSpecification.unrestricted(), DeleteSpecification::and); } /** - * Applies an OR operation to all the given {@link DeleteSpecification}s. + * 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. @@ -196,7 +205,8 @@ static DeleteSpecification anyOf(DeleteSpecification... specifications } /** - * Applies an OR operation to all the given {@link DeleteSpecification}s. + * 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. @@ -206,7 +216,7 @@ static DeleteSpecification anyOf(DeleteSpecification... specifications static DeleteSpecification anyOf(Iterable> specifications) { return StreamSupport.stream(specifications.spliterator(), false) // - .reduce(DeleteSpecification.all(), DeleteSpecification::or); + .reduce(DeleteSpecification.unrestricted(), DeleteSpecification::or); } /** 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 index f237715bc0..dc17edbfc4 100644 --- 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 @@ -30,6 +30,12 @@ /** * 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 @@ -42,7 +48,7 @@ public interface PredicateSpecification extends Serializable { * @param the type of the {@link Root} the resulting {@literal PredicateSpecification} operates on. * @return guaranteed to be not {@literal null}. */ - static PredicateSpecification all() { + static PredicateSpecification unrestricted() { return (root, builder) -> null; } @@ -104,13 +110,14 @@ static PredicateSpecification not(PredicateSpecification spec) { return (root, builder) -> { - Predicate not = spec.toPredicate(root, builder); - return not != null ? builder.not(not) : null; + Predicate predicate = spec.toPredicate(root, builder); + return predicate != null ? builder.not(predicate) : null; }; } /** - * Applies an AND operation to all the given {@link PredicateSpecification}s. + * 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. @@ -123,7 +130,8 @@ static PredicateSpecification allOf(PredicateSpecification... specific } /** - * Applies an AND operation to all the given {@link PredicateSpecification}s. + * 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. @@ -133,11 +141,12 @@ static PredicateSpecification allOf(PredicateSpecification... specific static PredicateSpecification allOf(Iterable> specifications) { return StreamSupport.stream(specifications.spliterator(), false) // - .reduce(PredicateSpecification.all(), PredicateSpecification::and); + .reduce(PredicateSpecification.unrestricted(), PredicateSpecification::and); } /** - * Applies an OR operation to all the given {@link PredicateSpecification}s. + * 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. @@ -150,7 +159,8 @@ static PredicateSpecification anyOf(PredicateSpecification... specific } /** - * Applies an OR operation to all the given {@link PredicateSpecification}s. + * 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. @@ -160,7 +170,7 @@ static PredicateSpecification anyOf(PredicateSpecification... specific static PredicateSpecification anyOf(Iterable> specifications) { return StreamSupport.stream(specifications.spliterator(), false) // - .reduce(PredicateSpecification.all(), PredicateSpecification::or); + .reduce(PredicateSpecification.unrestricted(), PredicateSpecification::or); } /** 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 975d52d6ec..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 @@ -32,6 +32,12 @@ /** * 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 @@ -51,7 +57,7 @@ public interface Specification extends Serializable { * @param the type of the {@link Root} the resulting {@literal Specification} operates on. * @return guaranteed to be not {@literal null}. */ - static Specification all() { + static Specification unrestricted() { return (root, query, builder) -> null; } @@ -148,13 +154,14 @@ static Specification not(Specification spec) { return (root, query, builder) -> { - Predicate not = spec.toPredicate(root, query, builder); - return not != null ? builder.not(not) : null; + 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. + * 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. @@ -168,7 +175,8 @@ static Specification allOf(Specification... specifications) { } /** - * Applies an AND 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. * @return the conjunction of the specifications. @@ -179,11 +187,12 @@ static Specification allOf(Specification... specifications) { static Specification allOf(Iterable> specifications) { return StreamSupport.stream(specifications.spliterator(), false) // - .reduce(Specification.all(), Specification::and); + .reduce(Specification.unrestricted(), Specification::and); } /** - * Applies an OR operation to all the given {@link Specification}s. + * 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 @@ -197,7 +206,8 @@ static Specification anyOf(Specification... specifications) { } /** - * Applies an OR operation to all the given {@link Specification}s. + * 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 @@ -208,7 +218,7 @@ static Specification anyOf(Specification... specifications) { static Specification anyOf(Iterable> specifications) { return StreamSupport.stream(specifications.spliterator(), false) // - .reduce(Specification.all(), Specification::or); + .reduce(Specification.unrestricted(), Specification::or); } /** 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 index 7667faa9c4..2e9d93b82a 100644 --- 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 @@ -31,6 +31,12 @@ /** * 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 @@ -39,27 +45,27 @@ public interface UpdateSpecification extends Serializable { /** - * Simple static factory method to create a specification deleting all objects. + * 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 all() { + static UpdateSpecification unrestricted() { return (root, query, builder) -> null; } /** - * Simple static factory method to add some syntactic sugar around a {@literal UpdateSpecification}. For example: + * Simple static factory method to add some syntactic sugar around a {@literal UpdateOperation}. For example: * *

-	 * UpdateSpecification<User> updateLastname = UpdateSpecification
+	 * 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 UpdateSpecification} operates on. + * @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}. */ @@ -172,13 +178,14 @@ static UpdateSpecification not(UpdateSpecification spec) { return (root, update, builder) -> { - Predicate not = spec.toPredicate(root, update, builder); - return not != null ? builder.not(not) : null; + 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. + * 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. @@ -191,7 +198,8 @@ static UpdateSpecification allOf(UpdateSpecification... specifications } /** - * Applies an AND operation to all the given {@link UpdateSpecification}s. + * 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. @@ -201,11 +209,12 @@ static UpdateSpecification allOf(UpdateSpecification... specifications static UpdateSpecification allOf(Iterable> specifications) { return StreamSupport.stream(specifications.spliterator(), false) // - .reduce(UpdateSpecification.all(), UpdateSpecification::and); + .reduce(UpdateSpecification.unrestricted(), UpdateSpecification::and); } /** - * Applies an OR operation to all the given {@link UpdateSpecification}s. + * 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. @@ -218,7 +227,8 @@ static UpdateSpecification anyOf(UpdateSpecification... specifications } /** - * Applies an OR operation to all the given {@link UpdateSpecification}s. + * 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. @@ -228,7 +238,7 @@ static UpdateSpecification anyOf(UpdateSpecification... specifications static UpdateSpecification anyOf(Iterable> specifications) { return StreamSupport.stream(specifications.spliterator(), false) // - .reduce(UpdateSpecification.all(), UpdateSpecification::or); + .reduce(UpdateSpecification.unrestricted(), UpdateSpecification::or); } /** 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 1965aaee7b..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 @@ -49,7 +49,7 @@ public interface JpaSpecificationExecutor { * @param spec must not be {@literal null}. * @return never {@literal null}. * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one entity found. - * @see Specification#all() + * @see Specification#unrestricted() */ default Optional findOne(PredicateSpecification spec) { return findOne(Specification.where(spec)); @@ -61,7 +61,7 @@ default Optional findOne(PredicateSpecification spec) { * @param spec must not be {@literal null}. * @return never {@literal null}. * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one entity found. - * @see Specification#all() + * @see Specification#unrestricted() */ Optional findOne(Specification spec); @@ -70,7 +70,7 @@ default Optional findOne(PredicateSpecification spec) { * * @param spec must not be {@literal null}. * @return never {@literal null}. - * @see Specification#all() + * @see Specification#unrestricted() */ default List findAll(PredicateSpecification spec) { return findAll(Specification.where(spec)); @@ -81,7 +81,7 @@ default List findAll(PredicateSpecification spec) { * * @param spec must not be {@literal null}. * @return never {@literal null}. - * @see Specification#all() + * @see Specification#unrestricted() */ List findAll(Specification spec); @@ -91,7 +91,7 @@ default List findAll(PredicateSpecification spec) { * @param spec must not be {@literal null}. * @param pageable must not be {@literal null}. * @return never {@literal null}. - * @see Specification#all() + * @see Specification#unrestricted() */ Page findAll(Specification spec, Pageable pageable); @@ -101,7 +101,7 @@ default List findAll(PredicateSpecification spec) { * @param spec must not be {@literal null}. * @param sort must not be {@literal null}. * @return never {@literal null}. - * @see Specification#all() + * @see Specification#unrestricted() */ List findAll(Specification spec, Sort sort); @@ -110,7 +110,7 @@ default List findAll(PredicateSpecification spec) { * * @param spec the {@link PredicateSpecification} to count instances for, must not be {@literal null}. * @return the number of instances. - * @see Specification#all() + * @see Specification#unrestricted() */ default long count(PredicateSpecification spec) { return count(Specification.where(spec)); @@ -121,7 +121,7 @@ default long count(PredicateSpecification spec) { * * @param spec the {@link Specification} to count instances for, must not be {@literal null}. * @return the number of instances. - * @see Specification#all() + * @see Specification#unrestricted() */ long count(Specification spec); @@ -131,7 +131,7 @@ default long count(PredicateSpecification spec) { * @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#all() + * @see Specification#unrestricted() */ default boolean exists(PredicateSpecification spec) { return exists(Specification.where(spec)); @@ -143,7 +143,7 @@ default boolean exists(PredicateSpecification spec) { * @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#all() + * @see Specification#unrestricted() */ boolean exists(Specification spec); @@ -168,7 +168,7 @@ default boolean exists(PredicateSpecification spec) { * @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#all() + * @see PredicateSpecification#unrestricted() */ default long delete(PredicateSpecification spec) { return delete(DeleteSpecification.where(spec)); @@ -183,7 +183,7 @@ default long delete(PredicateSpecification spec) { * @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#all() + * @see DeleteSpecification#unrestricted() */ long delete(DeleteSpecification spec); 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 3c137b4329..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 @@ -392,7 +392,7 @@ public boolean existsById(ID id) { @Override public List findAll() { - return getQuery(Specification.all(), Sort.unsorted()).getResultList(); + return getQuery(Specification.unrestricted(), Sort.unsorted()).getResultList(); } @Override @@ -425,12 +425,12 @@ public List findAllById(Iterable ids) { @Override public List findAll(Sort sort) { - return getQuery(Specification.all(), sort).getResultList(); + return getQuery(Specification.unrestricted(), sort).getResultList(); } @Override public Page findAll(Pageable pageable) { - return findAll(Specification.all(), pageable); + return findAll(Specification.unrestricted(), pageable); } @Override @@ -1016,7 +1016,7 @@ private static long executeCountQuery(TypedQuery query) { @SuppressWarnings("rawtypes") private static final class ByIdsSpecification implements Specification { - @Serial private static final @Serial long serialVersionUID = 1L; + private static final @Serial long serialVersionUID = 1L; private final JpaEntityInformation entityInformation; @@ -1046,7 +1046,7 @@ public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuild */ private static class ExampleSpecification implements Specification { - @Serial private static final @Serial 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 index 79e531ad7f..02e59fa2db 100644 --- 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 @@ -59,7 +59,7 @@ void setUp() { @Test // GH-3521 void allReturnsEmptyPredicate() { - DeleteSpecification specification = DeleteSpecification.all(); + DeleteSpecification specification = DeleteSpecification.unrestricted(); assertThat(specification).isNotNull(); assertThat(specification.toPredicate(root, delete, builder)).isNull(); 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 index f2f8a83a43..f0cd8ca085 100644 --- 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 @@ -57,7 +57,7 @@ void setUp() { @Test // GH-3521 void allReturnsEmptyPredicate() { - PredicateSpecification specification = PredicateSpecification.all(); + PredicateSpecification specification = PredicateSpecification.unrestricted(); assertThat(specification).isNotNull(); assertThat(specification.toPredicate(root, builder)).isNull(); 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 index f66bba7d73..540cc91e40 100644 --- 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 @@ -59,7 +59,7 @@ void setUp() { @Test // GH-3521 void allReturnsEmptyPredicate() { - UpdateSpecification specification = UpdateSpecification.all(); + UpdateSpecification specification = UpdateSpecification.unrestricted(); assertThat(specification).isNotNull(); assertThat(specification.toPredicate(root, update, builder)).isNull(); 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 e273400b57..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 @@ -592,7 +592,7 @@ void executesSimpleNotCorrectly() { void returnsSameListIfNoSpecGiven() { flushTestUsers(); - assertSameElements(repository.findAll(), repository.findAll(PredicateSpecification.all())); + assertSameElements(repository.findAll(), repository.findAll(PredicateSpecification.unrestricted())); } @Test @@ -608,7 +608,7 @@ void returnsSamePageIfNoSpecGiven() { Pageable pageable = PageRequest.of(0, 1); flushTestUsers(); - assertThat(repository.findAll(Specification.all(), pageable)).isEqualTo(repository.findAll(pageable)); + assertThat(repository.findAll(Specification.unrestricted(), pageable)).isEqualTo(repository.findAll(pageable)); } @Test // GH-3521 @@ -632,7 +632,7 @@ void predicateSpecificationRemovesAll() { flushTestUsers(); - repository.delete(DeleteSpecification.all()); + repository.delete(DeleteSpecification.unrestricted()); assertThat(repository.count()).isEqualTo(0L); } @@ -642,7 +642,7 @@ void deleteSpecificationRemovesAll() { flushTestUsers(); - repository.delete(DeleteSpecification.all()); + repository.delete(DeleteSpecification.unrestricted()); assertThat(repository.count()).isEqualTo(0L); } 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 85bd6b509f..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 @@ -210,7 +210,7 @@ void applyQueryHintsToCountQueriesForSpecificationPageables() { when(query.getResultList()).thenReturn(Arrays.asList(new User(), new User())); - repo.findAll(Specification.all(), PageRequest.of(2, 1)); + repo.findAll(Specification.unrestricted(), PageRequest.of(2, 1)); verify(metadata).getQueryHintsForCount(); }