From 8c5f169186f27cd294d4f6c8f606ef56adc2c496 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 13 Aug 2024 09:30:05 +0200 Subject: [PATCH] Explore refined Specification API. Introduce DeleteSpecification and UpdateSpecification. Add PredicateSpecification. Update SpecificationExecutor. Closes: #3521 Original Pull Request: #3578 --- .../data/jpa/domain/DeleteSpecification.java | 217 ++++++++++++ .../jpa/domain/PredicateSpecification.java | 174 ++++++++++ .../data/jpa/domain/Specification.java | 219 ++++++++---- .../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 | 2 - .../domain/UpdateSpecificationUnitTests.java | 170 ++++++++++ .../jpa/domain/sample/UserSpecifications.java | 17 +- .../jpa/repository/UserRepositoryTests.java | 72 ++-- .../support/SimpleJpaRepositoryUnitTests.java | 3 +- 14 files changed, 1711 insertions(+), 170 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 000000000..b3bfd93ae --- /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 000000000..b3e52f424 --- /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 9755052ed..7908ad1a7 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 @@ package org.springframework.data.jpa.domain; 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.Arrays; import java.util.stream.StreamSupport; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; /** * Specification in the sense of Domain Driven Design. @@ -44,6 +46,104 @@ public interface Specification 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 Specification} operates on. + * @return guaranteed to be not {@literal null}. + */ + 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 + * @deprecated since 3.5. + */ + @Deprecated(since = "3.5.0", forRemoval = true) + 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 the other {@link Specification}. + * @return the conjunction of the specifications. + * @since 2.0 + */ + 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 the other {@link Specification}. + * @return the disjunction of the specifications + * @since 2.0 + */ + default Specification or(Specification 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 + * @since 2.0 + */ + default Specification or(PredicateSpecification other) { + + Assert.notNull(other, "Other specification must not be null"); + + return SpecificationComposition.composed(this, where(other), CriteriaBuilder::or); + } + /** * Negates the given {@link Specification}. * @@ -52,80 +152,23 @@ public interface Specification extends Serializable { * @return guaranteed to be not {@literal null}. * @since 2.0 */ - static Specification not(@Nullable Specification spec) { + static Specification not(Specification spec) { - return spec == null // - ? (root, query, builder) -> null // - : (root, query, builder) -> { + Assert.notNull(spec, "Specification must not be null"); - Predicate predicate = spec.toPredicate(root, query, builder); - return predicate != null ? builder.not(predicate) : builder.disjunction(); + return (root, query, builder) -> { + + Predicate not = spec.toPredicate(root, query, builder); + return not != null ? builder.not(not) : 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}. - * @return guaranteed to be not {@literal null}. - * @since 2.0 - * @deprecated since 3.5. - */ - @Deprecated(since = "3.5.0", forRemoval = true) - static Specification where(@Nullable Specification spec) { - return spec == null ? (root, query, builder) -> null : spec; - } - - /** - * ANDs the given {@link Specification} to the current one. - * - * @param other can be {@literal null}. - * @return The conjunction of the specifications - * @since 2.0 - */ - default Specification and(@Nullable Specification other) { - return SpecificationComposition.composed(this, other, CriteriaBuilder::and); - } - - /** - * ORs the given specification to the current one. - * - * @param other can be {@literal null}. - * @return The disjunction of the specifications - * @since 2.0 - */ - default Specification or(@Nullable Specification other) { - 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}. - * - * @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}. - */ - @Nullable - Predicate toPredicate(Root root, @Nullable CriteriaQuery query, CriteriaBuilder criteriaBuilder); - /** * 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 conjunction of the specifications + * @param specifications the {@link Specification}s to compose. + * @return the conjunction of the specifications. * @see #and(Specification) - * @since 3.0 - */ - static Specification allOf(Iterable> specifications) { - - return StreamSupport.stream(specifications.spliterator(), false) // - .reduce(Specification.where(null), Specification::and); - } - - /** * @see #allOf(Iterable) * @since 3.0 */ @@ -135,20 +178,26 @@ public interface Specification extends Serializable { } /** - * 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 */ @@ -156,4 +205,32 @@ public interface Specification extends Serializable { 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 ad78749e3..0b6e90014 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 @@ class SpecificationComposition { } @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 000000000..8e217fc0f --- /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 c3249502e..da03816ec 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 @@ -29,9 +29,11 @@ import org.springframework.dao.InvalidDataAccessApiUsageException; 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. @@ -41,38 +43,65 @@ import org.springframework.lang.Nullable; * @author Diego Krupitza * @author Mark Paluch * @author Joshua Chen + * @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 Specification}. - *

- * If no {@link Specification} is given all entities matching {@code } will be selected. + * Returns all entities matching the given {@link PredicateSpecification}. * - * @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); + default List findAll(PredicateSpecification spec) { + return findAll(Specification.where(spec)); + } + + /** + * Returns all entities matching the given {@link Specification}. + * + * @param spec must not be {@literal null}. + * @return never {@literal null}. + * @see Specification#all() + */ + 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 a {@link Page} of entities matching the given {@link Specification}. @@ -92,52 +121,109 @@ public interface JpaSpecificationExecutor { /** * 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(@Nullable Specification spec); + 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() + */ + 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. - *

- * 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. * - * @param spec the {@link Specification} to use for the existence check, can not be {@literal null}. + * @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() */ - long delete(@Nullable Specification spec); + default long delete(PredicateSpecification spec) { + return delete(DeleteSpecification.where(spec)); + } + + /** + * Deletes by the {@link UpdateSpecification} 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 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 + */ + 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 25ffed505..2f18aeca9 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.TypedQuery; 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; @@ -52,7 +53,9 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.ScrollPosition; 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; @@ -398,7 +401,7 @@ public class SimpleJpaRepository implements JpaRepositoryImplementation findAll() { - return getQuery(null, Sort.unsorted()).getResultList(); + return getQuery(Specification.all(), Sort.unsorted()).getResultList(); } @Override @@ -431,12 +434,12 @@ public class SimpleJpaRepository implements JpaRepositoryImplementation 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 @@ -450,7 +453,7 @@ public class SimpleJpaRepository implements JpaRepositoryImplementation findAll(@Nullable Specification spec, Pageable pageable) { + public Page findAll(Specification spec, Pageable pageable) { return findAll(spec, spec, pageable); } @@ -463,13 +466,15 @@ public class SimpleJpaRepository implements JpaRepositoryImplementation 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)); @@ -482,21 +487,20 @@ public class SimpleJpaRepository implements JpaRepositoryImplementation spec) { + public long update(UpdateSpecification spec) { - CriteriaBuilder builder = this.entityManager.getCriteriaBuilder(); - CriteriaDelete delete = builder.createCriteriaDelete(getDomainClass()); + Assert.notNull(spec, "Specification must not be null"); - if (spec != null) { - Predicate predicate = spec.toPredicate(delete.from(getDomainClass()), builder.createQuery(getDomainClass()), - builder); + return getUpdate(spec, getDomainClass()).executeUpdate(); + } - if (predicate != null) { - delete.where(predicate); - } - } + @Override + @Transactional + public long delete(DeleteSpecification spec) { - return this.entityManager.createQuery(delete).executeUpdate(); + Assert.notNull(spec, "Specification must not be null"); + + return getDelete(spec, getDomainClass()).executeUpdate(); } @Override @@ -747,17 +751,17 @@ public class SimpleJpaRepository implements JpaRepositoryImplementation 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}. */ @@ -779,6 +783,8 @@ public class SimpleJpaRepository implements JpaRepositoryImplementation TypedQuery getQuery(ReturnedType returnedType, @Nullable Specification spec, Class domainClass, Sort sort, Collection inputProperties, @Nullable ScrollPosition scrollPosition) { + Assert.notNull(spec, "Specification must not be null"); + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); CriteriaQuery query; @@ -832,6 +838,42 @@ public class SimpleJpaRepository implements JpaRepositoryImplementation 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}. * @@ -883,25 +925,11 @@ public class SimpleJpaRepository implements JpaRepositoryImplementation 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); @@ -912,6 +940,32 @@ public class SimpleJpaRepository implements JpaRepositoryImplementation 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); + } + } + + 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) { if (metadata == null) { @@ -926,6 +980,20 @@ public class SimpleJpaRepository implements JpaRepositoryImplementation 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 000000000..f2f8a83a4 --- /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 eba6ed885..d45dd113d 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 @@ package org.springframework.data.jpa.domain; 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; 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 000000000..f66bba7d7 --- /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 304dcb560..cbd8ffd41 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 @@ import org.springframework.data.jpa.domain.Specification; */ 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 class UserSpecifications { }; } - 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 42f8e168d..4406270e6 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.assertj.core.api.Assertions.*; 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; @@ -62,7 +60,10 @@ import org.springframework.dao.InvalidDataAccessApiUsageException; 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; @@ -469,7 +470,7 @@ class UserRepositoryTests { void executesSpecificationCorrectly() { flushTestUsers(); - assertThat(repository.findAll(where(userHasFirstname("Oliver")))).hasSize(1); + assertThat(repository.findAll(Specification.where(userHasFirstname("Oliver")))).hasSize(1); } @Test @@ -499,11 +500,11 @@ class UserRepositoryTests { 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); @@ -516,7 +517,8 @@ class UserRepositoryTests { 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); } @@ -525,18 +527,18 @@ class UserRepositoryTests { 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); @@ -591,7 +593,7 @@ class UserRepositoryTests { void returnsSameListIfNoSpecGiven() { flushTestUsers(); - assertSameElements(repository.findAll(), repository.findAll((Specification) null)); + assertSameElements(repository.findAll(), repository.findAll(PredicateSpecification.all())); } @Test @@ -607,15 +609,41 @@ class UserRepositoryTests { 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-2796 - void removesAllIfSpecificationIsNull() { + @Test // GH-3521 + void updateSpecificationUpdatesMarriedEntities() { flushTestUsers(); - repository.delete((Specification) null); + 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 deleteSpecificationRemovesAll() { + + flushTestUsers(); + + repository.delete(DeleteSpecification.all()); assertThat(repository.count()).isEqualTo(0L); } @@ -3395,8 +3423,8 @@ class UserRepositoryTests { 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(); @@ -3421,7 +3449,7 @@ class UserRepositoryTests { flushTestUsers(); - Specification usersWithEInTheirName = userHasFirstnameLike("e"); + PredicateSpecification usersWithEInTheirName = userHasFirstnameLike("e"); long initialCount = repository.count(); assertThat(repository.delete(usersWithEInTheirName)).isEqualTo(3L); @@ -3568,16 +3596,16 @@ class UserRepositoryTests { 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 afd9634b4..ba3d60770 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 @@ -46,6 +46,7 @@ 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; @@ -218,7 +219,7 @@ class SimpleJpaRepositoryUnitTests { 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(); }