From c0cadfa40080b95e9bcda3df8cce74945dc5b891 Mon Sep 17 00:00:00 2001 From: "Greg L. Turnquist" Date: Thu, 5 May 2022 16:20:51 -0500 Subject: [PATCH] Introduce delete(Specification) on JpaSpecificationExecutor. Ever since JPA 2.1, CriteriaBuilder has offered createCriteriaDelete, returning a CriteriaDelete. With Spring Data JPA 3.0 rebased on JPA 3.0, we are able to guarantee this SPI, and hence rollout support for this feature. See #1262. --- .../data/jpa/domain/Specification.java | 1 + .../jpa/repository/JpaSpecificationExecutor.java | 8 ++++++++ .../repository/support/SimpleJpaRepository.java | 16 ++++++++++++++++ .../data/jpa/repository/UserRepositoryTests.java | 13 +++++++++++++ src/main/asciidoc/jpa.adoc | 15 +++++++++++++++ 5 files changed, 53 insertions(+) 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 5efadbf6b..c656cfa9f 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 @@ -16,6 +16,7 @@ package org.springframework.data.jpa.domain; import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaDelete; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; 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 b90b88d8f..0ba4d0371 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 @@ -87,6 +87,14 @@ public interface JpaSpecificationExecutor { */ boolean exists(Specification spec); + /** + * Deletes by the {@link Specification} and returns the number of rows deleted. + * + * @param spec the {@link Specification} to use for the existence check. Must not be {@literal null}. + * @return the number of entities deleted + */ + long delete(Specification spec); + /** * Returns entities matching the given {@link Specification} applying the {@code queryFunction} that defines the query * and its result type. 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 ebdec761b..4dd01244f 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 @@ -24,6 +24,7 @@ import jakarta.persistence.Parameter; import jakarta.persistence.Query; import jakarta.persistence.TypedQuery; import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaDelete; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.ParameterExpression; import jakarta.persistence.criteria.Path; @@ -485,6 +486,21 @@ public class SimpleJpaRepository implements JpaRepositoryImplementation spec) { + + CriteriaBuilder builder = this.em.getCriteriaBuilder(); + CriteriaDelete delete = builder.createCriteriaDelete(getDomainClass()); + + Predicate predicate = spec.toPredicate(delete.from(getDomainClass()), null, builder); + + if (predicate != null) { + delete.where(predicate); + } + + return this.em.createQuery(delete).executeUpdate(); + } + @Override public List findAll(Example example) { return getQuery(new ExampleSpecification<>(example, escapeCharacter), example.getProbeType(), Sort.unsorted()) 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 59b45711c..3099c04cb 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 @@ -2838,6 +2838,19 @@ public class UserRepositoryTests { assertThat(repository.exists(hundredYearsOld)).isTrue(); } + @Test // GH-1262 + void deleteWithSpec() { + + flushTestUsers(); + + Specification usersWithEInTheirName = userHasFirstnameLike("e"); + + long initialCount = repository.count(); + assertThat(repository.delete(usersWithEInTheirName)).isEqualTo(3L); + long finalCount = repository.count(); + assertThat(initialCount - finalCount).isEqualTo(3L); + } + private Page executeSpecWithSort(Sort sort) { flushTestUsers(); diff --git a/src/main/asciidoc/jpa.adoc b/src/main/asciidoc/jpa.adoc index f7e1fb04f..9d1a7c24b 100644 --- a/src/main/asciidoc/jpa.adoc +++ b/src/main/asciidoc/jpa.adoc @@ -943,6 +943,21 @@ List customers = customerRepository.findAll( `Specification` offers some "`glue-code`" default methods to chain and combine `Specification` instances. These methods let you extend your data access layer by creating new `Specification` implementations and combining them with already existing implementations. ==== +And with JPA 2.1, the `CriteriaBuilder` API introduced `CriteriaDelete`. This is provided through `JpaSpecificationExecutor`'s `delete(Specification)` API. + +.Using a `Specification` to delete entries. +==== +[source, java] +---- +Specification ageLessThan18 = (root, query, cb) -> cb.lessThan(root.get("age").as(Integer.class), 18) + +userRepository.delete(ageLessThan18); +---- +The `Specification` builds up a criteria where the `age` field (cast as an integer) is less than `18`. +Passed on to the `userRepository`, it will use JPA's `CriteriaDelete` feature to generate the right `DELETE` operation. +It then returns the number of entities deleted. +==== + include::{spring-data-commons-docs}/query-by-example.adoc[leveloffset=+1] include::query-by-example.adoc[leveloffset=+1]