Explore refined Specification API.

Introduce DeleteSpecification and UpdateSpecification. Add PredicateSpecification. Update SpecificationExecutor.

Closes: #3521
Original Pull Request: #3578
This commit is contained in:
Mark Paluch
2024-08-13 09:30:05 +02:00
parent 647b870276
commit 8c5f169186
14 changed files with 1711 additions and 170 deletions

View File

@@ -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<T> extends Serializable {
@Serial long serialVersionUID = 1L;
/**
* Simple static factory method to create a specification deleting all objects.
*
* @param <T> the type of the {@link Root} the resulting {@literal DeleteSpecification} operates on.
* @return guaranteed to be not {@literal null}.
*/
static <T> DeleteSpecification<T> all() {
return (root, query, builder) -> null;
}
/**
* Simple static factory method to add some syntactic sugar around a {@literal DeleteSpecification}.
*
* @param <T> 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 <T> DeleteSpecification<T> where(DeleteSpecification<T> 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 <T> 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 <T> DeleteSpecification<T> where(PredicateSpecification<T> 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<T> and(DeleteSpecification<T> 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<T> and(PredicateSpecification<T> 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<T> or(DeleteSpecification<T> 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<T> or(PredicateSpecification<T> other) {
Assert.notNull(other, "Other specification must not be null");
return SpecificationComposition.composed(this, where(other), CriteriaBuilder::or);
}
/**
* Negates the given {@link DeleteSpecification}.
*
* @param <T> 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 <T> DeleteSpecification<T> not(DeleteSpecification<T> 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 <T> DeleteSpecification<T> allOf(DeleteSpecification<T>... 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 <T> DeleteSpecification<T> allOf(Iterable<DeleteSpecification<T>> 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 <T> DeleteSpecification<T> anyOf(DeleteSpecification<T>... 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 <T> DeleteSpecification<T> anyOf(Iterable<DeleteSpecification<T>> 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<T> root, CriteriaDelete<T> delete, CriteriaBuilder criteriaBuilder);
}

View File

@@ -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<T> extends Serializable {
@Serial long serialVersionUID = 1L;
/**
* Simple static factory method to create a specification matching all objects.
*
* @param <T> the type of the {@link Root} the resulting {@literal PredicateSpecification} operates on.
* @return guaranteed to be not {@literal null}.
*/
static <T> PredicateSpecification<T> all() {
return (root, builder) -> null;
}
/**
* Simple static factory method to add some syntactic sugar around a {@literal PredicateSpecification}.
*
* @param <T> 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 <T> PredicateSpecification<T> where(PredicateSpecification<T> 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<T> and(PredicateSpecification<T> 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<T> or(PredicateSpecification<T> other) {
Assert.notNull(other, "Other specification must not be null");
return SpecificationComposition.composed(this, other, CriteriaBuilder::or);
}
/**
* Negates the given {@link PredicateSpecification}.
*
* @param <T> 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 <T> PredicateSpecification<T> not(PredicateSpecification<T> 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 <T> PredicateSpecification<T> allOf(PredicateSpecification<T>... 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 <T> PredicateSpecification<T> allOf(Iterable<PredicateSpecification<T>> 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 <T> PredicateSpecification<T> anyOf(PredicateSpecification<T>... 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 <T> PredicateSpecification<T> anyOf(Iterable<PredicateSpecification<T>> 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<T> root, CriteriaBuilder criteriaBuilder);
}

View File

@@ -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<T> extends Serializable {
@Serial long serialVersionUID = 1L;
/**
* Simple static factory method to create a specification matching all objects.
*
* @param <T> the type of the {@link Root} the resulting {@literal Specification} operates on.
* @return guaranteed to be not {@literal null}.
*/
static <T> Specification<T> all() {
return (root, query, builder) -> null;
}
/**
* Simple static factory method to add some syntactic sugar around a {@link Specification}.
*
* @param <T> 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 <T> Specification<T> where(Specification<T> 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 <T> 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 <T> Specification<T> where(PredicateSpecification<T> 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<T> and(Specification<T> 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<T> and(PredicateSpecification<T> 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<T> or(Specification<T> 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<T> or(PredicateSpecification<T> 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<T> extends Serializable {
* @return guaranteed to be not {@literal null}.
* @since 2.0
*/
static <T> Specification<T> not(@Nullable Specification<T> spec) {
static <T> Specification<T> not(Specification<T> 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 <T> 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 <T> Specification<T> where(@Nullable Specification<T> 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<T> and(@Nullable Specification<T> 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<T> or(@Nullable Specification<T> 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<T> 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 <T> Specification<T> allOf(Iterable<Specification<T>> 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<T> 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 <T> Specification<T> anyOf(Iterable<Specification<T>> specifications) {
static <T> Specification<T> allOf(Iterable<Specification<T>> 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<T> extends Serializable {
static <T> Specification<T> anyOf(Specification<T>... 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 <T> Specification<T> anyOf(Iterable<Specification<T>> 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<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder);
}

View File

@@ -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 <T> Predicate toPredicate(@Nullable Specification<T> specification, Root<T> root, @Nullable CriteriaQuery<?> query,
CriteriaBuilder builder) {
private static <T> Predicate toPredicate(@Nullable Specification<T> specification, Root<T> root,
@Nullable CriteriaQuery<?> query, CriteriaBuilder builder) {
return specification == null ? null : specification.toPredicate(root, query, builder);
}
static <T> DeleteSpecification<T> composed(@Nullable DeleteSpecification<T> lhs, @Nullable DeleteSpecification<T> 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 <T> Predicate toPredicate(@Nullable DeleteSpecification<T> specification, Root<T> root,
@Nullable CriteriaDelete<T> delete, CriteriaBuilder builder) {
return specification == null ? null : specification.toPredicate(root, delete, builder);
}
static <T> UpdateSpecification<T> composed(@Nullable UpdateSpecification<T> lhs, @Nullable UpdateSpecification<T> 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 <T> Predicate toPredicate(@Nullable UpdateSpecification<T> specification, Root<T> root,
CriteriaUpdate<T> update, CriteriaBuilder builder) {
return specification == null ? null : specification.toPredicate(root, update, builder);
}
static <T> PredicateSpecification<T> composed(PredicateSpecification<T> lhs, PredicateSpecification<T> 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 <T> Predicate toPredicate(@Nullable PredicateSpecification<T> specification, Root<T> root,
CriteriaBuilder builder) {
return specification == null ? null : specification.toPredicate(root, builder);
}
}

View File

@@ -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<T> extends Serializable {
@Serial long serialVersionUID = 1L;
/**
* Simple static factory method to create a specification deleting all objects.
*
* @param <T> the type of the {@link Root} the resulting {@literal UpdateSpecification} operates on.
* @return guaranteed to be not {@literal null}.
*/
static <T> UpdateSpecification<T> all() {
return (root, query, builder) -> null;
}
/**
* Simple static factory method to add some syntactic sugar around a {@literal UpdateSpecification}. For example:
*
* <pre class="code">
* UpdateSpecification&lt;User&gt; updateLastname = UpdateSpecification
* .&lt;User&gt; update((root, update, criteriaBuilder) -> update.set("lastname", "Heisenberg"))
* .where(userHasFirstname("Walter").and(userHasLastname("White")));
*
* repository.update(updateLastname);
* </pre>
*
* @param <T> 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 <T> UpdateOperation<T> update(UpdateOperation<T> 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 <T> 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 <T> UpdateSpecification<T> where(UpdateSpecification<T> 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 <T> 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 <T> UpdateSpecification<T> where(PredicateSpecification<T> 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<T> and(UpdateSpecification<T> 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<T> and(PredicateSpecification<T> 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<T> or(UpdateSpecification<T> 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<T> or(PredicateSpecification<T> other) {
Assert.notNull(other, "Other specification must not be null");
return SpecificationComposition.composed(this, where(other), CriteriaBuilder::or);
}
/**
* Negates the given {@link UpdateSpecification}.
*
* @param <T> 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 <T> UpdateSpecification<T> not(UpdateSpecification<T> 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 <T> UpdateSpecification<T> allOf(UpdateSpecification<T>... 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 <T> UpdateSpecification<T> allOf(Iterable<UpdateSpecification<T>> 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 <T> UpdateSpecification<T> anyOf(UpdateSpecification<T>... 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 <T> UpdateSpecification<T> anyOf(Iterable<UpdateSpecification<T>> 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<T> root, CriteriaUpdate<T> 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:
*
* <pre class="code">
* UpdateSpecification&lt;User&gt; updateLastname = UpdateSpecification
* .&lt;User&gt; update((root, update, criteriaBuilder) -> update.set("lastname", "Heisenberg"))
* .where(userHasFirstname("Walter").and(userHasLastname("White")));
*
* repository.update(updateLastname);
* </pre>
*
* @param <T>
*/
@FunctionalInterface
interface UpdateOperation<T> {
/**
* ANDs the given {@link UpdateOperation} to the current one.
*
* @param other the other {@link UpdateOperation}.
* @return the conjunction of the specifications.
*/
default UpdateOperation<T> and(UpdateOperation<T> 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<T> where(PredicateSpecification<T> 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<T> where(UpdateSpecification<T> 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<T> root, CriteriaUpdate<T> update, CriteriaBuilder criteriaBuilder);
}
}

View File

@@ -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<T> {
/**
* 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<T> findOne(PredicateSpecification<T> 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<T> findOne(Specification<T> spec);
/**
* Returns all entities matching the given {@link Specification}.
* <p>
* If no {@link Specification} is given all entities matching {@code <T>} 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<T> findAll(@Nullable Specification<T> spec);
default List<T> findAll(PredicateSpecification<T> 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<T> findAll(Specification<T> spec);
/**
* Returns a {@link Page} of entities matching the given {@link Specification}.
* <p>
* If no {@link Specification} is given all entities matching {@code <T>} 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<T> findAll(@Nullable Specification<T> spec, Pageable pageable);
Page<T> findAll(Specification<T> spec, Pageable pageable);
/**
* Returns a {@link Page} of entities matching the given {@link Specification}.
@@ -92,52 +121,109 @@ public interface JpaSpecificationExecutor<T> {
/**
* Returns all entities matching the given {@link Specification} and {@link Sort}.
* <p>
* If no {@link Specification} is given all entities matching {@code <T>} 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<T> findAll(@Nullable Specification<T> spec, Sort sort);
List<T> findAll(Specification<T> 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<T> spec) {
return count(Specification.where(spec));
}
/**
* Returns the number of instances that the given {@link Specification} will return.
* <p>
* If no {@link Specification} is given all entities matching {@code <T>} 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<T> spec);
long count(Specification<T> 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<T> 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<T> 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.
* <p>
* 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<T> spec);
/**
* Deletes by the {@link PredicateSpecification} and returns the number of rows deleted.
* <p>
* 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.
* <p>
* 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}.
* <p>
* If no {@link Specification} is given all entities matching {@code <T>} 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<T> spec);
default long delete(PredicateSpecification<T> spec) {
return delete(DeleteSpecification.where(spec));
}
/**
* Deletes by the {@link UpdateSpecification} and returns the number of rows deleted.
* <p>
* 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<T> 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 <S extends T, R> R findBy(PredicateSpecification<T> spec,
Function<FluentQuery.FetchableFluentQuery<S>, R> queryFunction) {
return findBy(Specification.where(spec), queryFunction);
}
/**
* Returns entities matching the given {@link Specification} applying the {@code queryFunction} that defines the query

View File

@@ -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<T, ID> implements JpaRepositoryImplementation<T
@Override
public List<T> findAll() {
return getQuery(null, Sort.unsorted()).getResultList();
return getQuery(Specification.all(), Sort.unsorted()).getResultList();
}
@Override
@@ -431,12 +434,12 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
@Override
public List<T> findAll(Sort sort) {
return getQuery(null, sort).getResultList();
return getQuery(Specification.all(), sort).getResultList();
}
@Override
public Page<T> findAll(Pageable pageable) {
return findAll((Specification<T>) null, pageable);
return findAll(Specification.all(), pageable);
}
@Override
@@ -450,7 +453,7 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
}
@Override
public Page<T> findAll(@Nullable Specification<T> spec, Pageable pageable) {
public Page<T> findAll(Specification<T> spec, Pageable pageable) {
return findAll(spec, spec, pageable);
}
@@ -463,13 +466,15 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
}
@Override
public List<T> findAll(@Nullable Specification<T> spec, Sort sort) {
public List<T> findAll(Specification<T> spec, Sort sort) {
return getQuery(spec, sort).getResultList();
}
@Override
public boolean exists(Specification<T> spec) {
Assert.notNull(spec, "Specification must not be null");
CriteriaQuery<Integer> cq = this.entityManager.getCriteriaBuilder() //
.createQuery(Integer.class) //
.select(this.entityManager.getCriteriaBuilder().literal(1));
@@ -482,21 +487,20 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
@Override
@Transactional
public long delete(@Nullable Specification<T> spec) {
public long update(UpdateSpecification<T> spec) {
CriteriaBuilder builder = this.entityManager.getCriteriaBuilder();
CriteriaDelete<T> 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<T> 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<T, ID> implements JpaRepositoryImplementation<T
/**
* 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<T> getQuery(@Nullable Specification<T> spec, Sort sort) {
protected TypedQuery<T> getQuery(Specification<T> 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<T, ID> implements JpaRepositoryImplementation<T
private <S extends T> TypedQuery<S> getQuery(ReturnedType returnedType, @Nullable Specification<S> spec,
Class<S> domainClass, Sort sort, Collection<String> inputProperties, @Nullable ScrollPosition scrollPosition) {
Assert.notNull(spec, "Specification must not be null");
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<S> query;
@@ -832,6 +838,42 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
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 <S> Query getUpdate(UpdateSpecification<S> spec, Class<S> domainClass) {
Assert.notNull(spec, "Specification must not be null");
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaUpdate<S> 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 <S> Query getDelete(DeleteSpecification<S> spec, Class<S> domainClass) {
Assert.notNull(spec, "Specification must not be null");
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaDelete<S> 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<T, ID> implements JpaRepositoryImplementation<T
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 <S, U extends T> Root<U> applySpecificationToCriteria(@Nullable Specification<U> spec, Class<U> domainClass,
private <S, U extends T> Root<U> applySpecificationToCriteria(Specification<U> spec, Class<U> domainClass,
CriteriaQuery<S> query) {
Assert.notNull(domainClass, "Domain class must not be null");
Assert.notNull(query, "CriteriaQuery must not be null");
Root<U> 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<T, ID> implements JpaRepositoryImplementation<T
return root;
}
private <S> void applySpecificationToCriteria(UpdateSpecification<S> spec, Class<S> domainClass,
CriteriaUpdate<S> query) {
Root<S> root = query.from(domainClass);
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
Predicate predicate = spec.toPredicate(root, query, builder);
if (predicate != null) {
query.where(predicate);
}
}
private <S> void applySpecificationToCriteria(DeleteSpecification<S> spec, Class<S> domainClass,
CriteriaDelete<S> query) {
Root<S> root = query.from(domainClass);
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
Predicate predicate = spec.toPredicate(root, query, builder);
if (predicate != null) {
query.where(predicate);
}
}
private <S> TypedQuery<S> applyRepositoryMethodMetadata(TypedQuery<S> query) {
if (metadata == null) {
@@ -926,6 +980,20 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
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) {

View File

@@ -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<Object> spec;
@Mock(serializable = true) Root<Object> root;
@Mock(serializable = true) CriteriaDelete<Object> 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<Object> specification = DeleteSpecification.all();
assertThat(specification).isNotNull();
assertThat(specification.toPredicate(root, delete, builder)).isNull();
}
@Test // GH-3521
void allOfCombinesPredicatesInOrder() {
DeleteSpecification<Object> specification = DeleteSpecification.allOf(spec);
assertThat(specification).isNotNull();
assertThat(specification.toPredicate(root, delete, builder)).isSameAs(predicate);
}
@Test // GH-3521
void anyOfCombinesPredicatesInOrder() {
DeleteSpecification<Object> specification = DeleteSpecification.allOf(spec);
assertThat(specification).isNotNull();
assertThat(specification.toPredicate(root, delete, builder)).isSameAs(predicate);
}
@Test // GH-3521
void emptyAllOfReturnsEmptySpecification() {
DeleteSpecification<Object> specification = DeleteSpecification.allOf();
assertThat(specification).isNotNull();
assertThat(specification.toPredicate(root, delete, builder)).isNull();
}
@Test // GH-3521
void emptyAnyOfReturnsEmptySpecification() {
DeleteSpecification<Object> specification = DeleteSpecification.anyOf();
assertThat(specification).isNotNull();
assertThat(specification.toPredicate(root, delete, builder)).isNull();
}
@Test // GH-3521
void specificationsShouldBeSerializable() {
DeleteSpecification<Object> serializableSpec = new SerializableSpecification();
DeleteSpecification<Object> specification = serializableSpec.and(serializableSpec);
assertThat(specification).isNotNull();
@SuppressWarnings("unchecked")
DeleteSpecification<Object> transferredSpecification = (DeleteSpecification<Object>) deserialize(
serialize(specification));
assertThat(transferredSpecification).isNotNull();
}
@Test // GH-3521
void complexSpecificationsShouldBeSerializable() {
SerializableSpecification serializableSpec = new SerializableSpecification();
DeleteSpecification<Object> specification = DeleteSpecification
.not(serializableSpec.and(serializableSpec).or(serializableSpec));
assertThat(specification).isNotNull();
@SuppressWarnings("unchecked")
DeleteSpecification<Object> transferredSpecification = (DeleteSpecification<Object>) deserialize(
serialize(specification));
assertThat(transferredSpecification).isNotNull();
}
@Test // GH-3521
void andCombinesSpecificationsInOrder() {
Predicate firstPredicate = mock(Predicate.class);
Predicate secondPredicate = mock(Predicate.class);
DeleteSpecification<Object> first = ((root1, delete, criteriaBuilder) -> firstPredicate);
DeleteSpecification<Object> 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<Object> first = ((root1, delete, criteriaBuilder) -> firstPredicate);
DeleteSpecification<Object> second = ((root1, delete, criteriaBuilder) -> secondPredicate);
first.or(second).toPredicate(root, delete, builder);
verify(builder).or(firstPredicate, secondPredicate);
}
static class SerializableSpecification implements Serializable, DeleteSpecification<Object> {
@Override
public Predicate toPredicate(Root<Object> root, CriteriaDelete<Object> delete, CriteriaBuilder cb) {
return null;
}
}
}

View File

@@ -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<Object> spec;
@Mock(serializable = true) Root<Object> 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<Object> specification = PredicateSpecification.all();
assertThat(specification).isNotNull();
assertThat(specification.toPredicate(root, builder)).isNull();
}
@Test // GH-3521
void allOfCombinesPredicatesInOrder() {
PredicateSpecification<Object> specification = PredicateSpecification.allOf(spec);
assertThat(specification).isNotNull();
assertThat(specification.toPredicate(root, builder)).isSameAs(predicate);
}
@Test // GH-3521
void anyOfCombinesPredicatesInOrder() {
PredicateSpecification<Object> specification = PredicateSpecification.allOf(spec);
assertThat(specification).isNotNull();
assertThat(specification.toPredicate(root, builder)).isSameAs(predicate);
}
@Test // GH-3521
void emptyAllOfReturnsEmptySpecification() {
PredicateSpecification<Object> specification = PredicateSpecification.allOf();
assertThat(specification).isNotNull();
assertThat(specification.toPredicate(root, builder)).isNull();
}
@Test // GH-3521
void emptyAnyOfReturnsEmptySpecification() {
PredicateSpecification<Object> specification = PredicateSpecification.anyOf();
assertThat(specification).isNotNull();
assertThat(specification.toPredicate(root, builder)).isNull();
}
@Test // GH-3521
void specificationsShouldBeSerializable() {
PredicateSpecification<Object> serializableSpec = new SerializableSpecification();
PredicateSpecification<Object> specification = serializableSpec.and(serializableSpec);
assertThat(specification).isNotNull();
@SuppressWarnings("unchecked")
PredicateSpecification<Object> transferredSpecification = (PredicateSpecification<Object>) deserialize(
serialize(specification));
assertThat(transferredSpecification).isNotNull();
}
@Test // GH-3521
void complexSpecificationsShouldBeSerializable() {
SerializableSpecification serializableSpec = new SerializableSpecification();
PredicateSpecification<Object> specification = PredicateSpecification
.not(serializableSpec.and(serializableSpec).or(serializableSpec));
assertThat(specification).isNotNull();
@SuppressWarnings("unchecked")
PredicateSpecification<Object> transferredSpecification = (PredicateSpecification<Object>) deserialize(
serialize(specification));
assertThat(transferredSpecification).isNotNull();
}
@Test // GH-3521
void andCombinesSpecificationsInOrder() {
Predicate firstPredicate = mock(Predicate.class);
Predicate secondPredicate = mock(Predicate.class);
PredicateSpecification<Object> first = ((root1, criteriaBuilder) -> firstPredicate);
PredicateSpecification<Object> 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<Object> first = ((root1, criteriaBuilder) -> firstPredicate);
PredicateSpecification<Object> second = ((root1, criteriaBuilder) -> secondPredicate);
first.or(second).toPredicate(root, builder);
verify(builder).or(firstPredicate, secondPredicate);
}
static class SerializableSpecification implements Serializable, PredicateSpecification<Object> {
@Override
public Predicate toPredicate(Root<Object> root, CriteriaBuilder cb) {
return null;
}
}
}

View File

@@ -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;

View File

@@ -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<Object> spec;
@Mock(serializable = true) Root<Object> root;
@Mock(serializable = true) CriteriaUpdate<Object> 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<Object> specification = UpdateSpecification.all();
assertThat(specification).isNotNull();
assertThat(specification.toPredicate(root, update, builder)).isNull();
}
@Test // GH-3521
void allOfCombinesPredicatesInOrder() {
UpdateSpecification<Object> specification = UpdateSpecification.allOf(spec);
assertThat(specification).isNotNull();
assertThat(specification.toPredicate(root, update, builder)).isSameAs(predicate);
}
@Test // GH-3521
void anyOfCombinesPredicatesInOrder() {
UpdateSpecification<Object> specification = UpdateSpecification.allOf(spec);
assertThat(specification).isNotNull();
assertThat(specification.toPredicate(root, update, builder)).isSameAs(predicate);
}
@Test // GH-3521
void emptyAllOfReturnsEmptySpecification() {
UpdateSpecification<Object> specification = UpdateSpecification.allOf();
assertThat(specification).isNotNull();
assertThat(specification.toPredicate(root, update, builder)).isNull();
}
@Test // GH-3521
void emptyAnyOfReturnsEmptySpecification() {
UpdateSpecification<Object> specification = UpdateSpecification.anyOf();
assertThat(specification).isNotNull();
assertThat(specification.toPredicate(root, update, builder)).isNull();
}
@Test // GH-3521
void specificationsShouldBeSerializable() {
UpdateSpecification<Object> serializableSpec = new SerializableSpecification();
UpdateSpecification<Object> specification = serializableSpec.and(serializableSpec);
assertThat(specification).isNotNull();
@SuppressWarnings("unchecked")
UpdateSpecification<Object> transferredSpecification = (UpdateSpecification<Object>) deserialize(
serialize(specification));
assertThat(transferredSpecification).isNotNull();
}
@Test // GH-3521
void complexSpecificationsShouldBeSerializable() {
SerializableSpecification serializableSpec = new SerializableSpecification();
UpdateSpecification<Object> specification = UpdateSpecification
.not(serializableSpec.and(serializableSpec).or(serializableSpec));
assertThat(specification).isNotNull();
@SuppressWarnings("unchecked")
UpdateSpecification<Object> transferredSpecification = (UpdateSpecification<Object>) deserialize(
serialize(specification));
assertThat(transferredSpecification).isNotNull();
}
@Test // GH-3521
void andCombinesSpecificationsInOrder() {
Predicate firstPredicate = mock(Predicate.class);
Predicate secondPredicate = mock(Predicate.class);
UpdateSpecification<Object> first = ((root1, update, criteriaBuilder) -> firstPredicate);
UpdateSpecification<Object> 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<Object> first = ((root1, update, criteriaBuilder) -> firstPredicate);
UpdateSpecification<Object> second = ((root1, update, criteriaBuilder) -> secondPredicate);
first.or(second).toPredicate(root, update, builder);
verify(builder).or(firstPredicate, secondPredicate);
}
static class SerializableSpecification implements Serializable, UpdateSpecification<Object> {
@Override
public Predicate toPredicate(Root<Object> root, CriteriaUpdate<Object> update, CriteriaBuilder cb) {
return null;
}
}
}

View File

@@ -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<User> userHasFirstname(final String firstname) {
public static PredicateSpecification<User> userHasFirstname(final String firstname) {
return simplePropertySpec("firstname", firstname);
}
public static Specification<User> userHasLastname(final String lastname) {
public static PredicateSpecification<User> userHasLastname(final String lastname) {
return simplePropertySpec("lastname", lastname);
}
public static Specification<User> userHasFirstnameLike(final String expression) {
public static PredicateSpecification<User> 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<User> userHasAgeLess(final Integer age) {
public static PredicateSpecification<User> 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<User> userHasLastnameLikeWithSort(final String expression) {
@@ -55,8 +56,8 @@ public class UserSpecifications {
};
}
private static <T> Specification<T> simplePropertySpec(final String property, final Object value) {
private static <T> PredicateSpecification<T> 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);
}
}

View File

@@ -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<User> spec1 = userHasFirstname("Oliver").or(userHasLastname("Arrasz"));
PredicateSpecification<User> spec1 = userHasFirstname("Oliver").or(userHasLastname("Arrasz"));
List<User> users1 = repository.findAll(spec1);
assertThat(users1).hasSize(2);
Specification<User> spec2 = Specification.anyOf( //
PredicateSpecification<User> spec2 = PredicateSpecification.anyOf( //
userHasFirstname("Oliver"), //
userHasLastname("Arrasz"));
List<User> users2 = repository.findAll(spec2);
@@ -516,7 +517,8 @@ class UserRepositoryTests {
void executesNegatingSpecificationCorrectly() {
flushTestUsers();
Specification<User> spec = not(userHasFirstname("Oliver")).and(userHasLastname("Arrasz"));
PredicateSpecification<User> spec = PredicateSpecification.not(userHasFirstname("Oliver"))
.and(userHasLastname("Arrasz"));
assertThat(repository.findAll(spec)).containsOnly(secondUser);
}
@@ -525,18 +527,18 @@ class UserRepositoryTests {
void executesCombinedSpecificationsWithPageableCorrectly() {
flushTestUsers();
Specification<User> spec1 = userHasFirstname("Oliver").or(userHasLastname("Arrasz"));
PredicateSpecification<User> spec1 = userHasFirstname("Oliver").or(userHasLastname("Arrasz"));
Page<User> users1 = repository.findAll(spec1, PageRequest.of(0, 1));
Page<User> users1 = repository.findAll(Specification.where(spec1), PageRequest.of(0, 1));
assertThat(users1.getSize()).isOne();
assertThat(users1.hasPrevious()).isFalse();
assertThat(users1.getTotalElements()).isEqualTo(2L);
Specification<User> spec2 = Specification.anyOf( //
PredicateSpecification<User> spec2 = PredicateSpecification.anyOf( //
userHasFirstname("Oliver"), //
userHasLastname("Arrasz"));
Page<User> users2 = repository.findAll(spec2, PageRequest.of(0, 1));
Page<User> 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<User>) 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<User>) 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<User>) null);
UpdateSpecification<User> updateLastname = UpdateSpecification.<User> 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<User> minorSpec = userHasAgeLess(18);
Specification<User> hundredYearsOld = userHasAgeLess(100);
PredicateSpecification<User> minorSpec = userHasAgeLess(18);
PredicateSpecification<User> hundredYearsOld = userHasAgeLess(100);
assertThat(repository.exists(minorSpec)).isFalse();
assertThat(repository.exists(hundredYearsOld)).isTrue();
@@ -3421,7 +3449,7 @@ class UserRepositoryTests {
flushTestUsers();
Specification<User> usersWithEInTheirName = userHasFirstnameLike("e");
PredicateSpecification<User> usersWithEInTheirName = userHasFirstnameLike("e");
long initialCount = repository.count();
assertThat(repository.delete(usersWithEInTheirName)).isEqualTo(3L);
@@ -3568,16 +3596,16 @@ class UserRepositoryTests {
flushTestUsers();
Specification<User> spec1 = userHasFirstname("Oliver").or(userHasLastname("Matthews"));
PredicateSpecification<User> spec1 = userHasFirstname("Oliver").or(userHasLastname("Matthews"));
Page<User> result1 = repository.findAll(spec1, PageRequest.of(0, 1, sort));
Page<User> result1 = repository.findAll(Specification.where(spec1), PageRequest.of(0, 1, sort));
assertThat(result1.getTotalElements()).isEqualTo(2L);
Specification<User> spec2 = Specification.anyOf( //
PredicateSpecification<User> spec2 = PredicateSpecification.anyOf( //
userHasFirstname("Oliver"), //
userHasLastname("Matthews"));
Page<User> result2 = repository.findAll(spec2, PageRequest.of(0, 1, sort));
Page<User> result2 = repository.findAll(Specification.where(spec2), PageRequest.of(0, 1, sort));
assertThat(result2.getTotalElements()).isEqualTo(2L);
assertThat(result1).containsExactlyElementsOf(result2);

View File

@@ -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();
}