|
1 | 1 | [[specifications]] |
2 | 2 | = Specifications |
3 | 3 |
|
4 | | -JPA 2 introduces a criteria API that you can use to build queries programmatically. By writing a `criteria`, you define the where clause of a query for a domain class. Taking another step back, these criteria can be regarded as a predicate over the entity that is described by the JPA criteria API constraints. |
5 | | - |
6 | | -Spring Data JPA takes the concept of a specification from Eric Evans' book, "`Domain Driven Design`", following the same semantics and providing an API to define such specifications with the JPA criteria API. To support specifications, you can extend your repository interface with the `JpaSpecificationExecutor` interface, as follows: |
| 4 | +JPA's Criteria API lets you build queries programmatically. |
| 5 | +Spring Data JPA `Specification` provides a small, focused API to express predicates over entities and reuse them across repositories. |
| 6 | +Based on the concept of a specification from Eric Evans' book "`Domain Driven Design`", specifications follow the same semantics providing an API to define criteria using JPA. |
| 7 | +To support specifications, you can extend your repository interface with the `JpaSpecificationExecutor` interface, as follows: |
7 | 8 |
|
8 | 9 | [source, java] |
9 | 10 | ---- |
10 | 11 | public interface CustomerRepository extends CrudRepository<Customer, Long>, JpaSpecificationExecutor<Customer> { |
11 | | - … |
12 | 12 | } |
13 | 13 | ---- |
14 | 14 |
|
15 | | -The additional interface has methods that let you run specifications in a variety of ways. For example, the `findAll` method returns all entities that match the specification, as shown in the following example: |
| 15 | +A specification is a predicate over an entity expressed with the Criteria API. |
| 16 | +Spring Data JPA offers two entry points: |
16 | 17 |
|
17 | | -[source, java] |
18 | | ----- |
19 | | -List<T> findAll(Specification<T> spec); |
20 | | ----- |
| 18 | +* <<predicate-specification,`PredicateSpecification`>>: A flexible, query-type-agnostic interface introduced with Spring Data JPA 4.0. |
| 19 | +* <<specification-interfaces,`Specification`>> (and `UpdateSpecification`, `DeleteSpecification`): Query-bound variants. |
| 20 | + |
| 21 | +[[predicate-specification]] |
| 22 | +== PredicateSpecification |
21 | 23 |
|
22 | | -The `Specification` interface is defined as follows: |
| 24 | +The `PredicateSpecification` interface is defined with a minimal set of dependencies allowing broad functional composition: |
23 | 25 |
|
24 | 26 | [source, java] |
25 | 27 | ---- |
26 | | -public interface Specification<T> { |
27 | | - Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, |
28 | | - CriteriaBuilder builder); |
| 28 | +public interface PredicateSpecification<T> { |
| 29 | + Predicate toPredicate(From<?, T> from, CriteriaBuilder builder); |
29 | 30 | } |
30 | 31 | ---- |
31 | 32 |
|
32 | | -Specifications can easily be used to build an extensible set of predicates on top of an entity that then can be combined and used with `JpaRepository` without the need to declare a query (method) for every needed combination, as shown in the following example: |
| 33 | +Specifications can easily be used to build an extensible set of predicates and used with `JpaRepository` removing the need to declare a query (method) for every needed combination as shown in the following example: |
33 | 34 |
|
34 | 35 | .Specifications for a Customer |
35 | 36 | ==== |
36 | | -[source, java] |
| 37 | +[source,java] |
37 | 38 | ---- |
38 | | -public class CustomerSpecs { |
39 | | -
|
| 39 | +class CustomerSpecs { |
40 | 40 |
|
41 | | - public static Specification<Customer> isLongTermCustomer() { |
42 | | - return (root, query, builder) -> { |
| 41 | + static PredicateSpecification<Customer> isLongTermCustomer() { |
| 42 | + return (from, builder) -> { |
43 | 43 | LocalDate date = LocalDate.now().minusYears(2); |
44 | | - return builder.lessThan(root.get(Customer_.createdAt), date); |
| 44 | + return builder.lessThan(from.get(Customer_.createdAt), date); |
45 | 45 | }; |
46 | 46 | } |
47 | 47 |
|
48 | | - public static Specification<Customer> hasSalesOfMoreThan(MonetaryAmount value) { |
49 | | - return (root, query, builder) -> { |
50 | | - // build query here |
| 48 | + static PredicateSpecification<Customer> hasSalesOfMoreThan(MonetaryAmount value) { |
| 49 | + return (from, builder) -> { |
| 50 | + // build predicate for sales > value |
51 | 51 | }; |
52 | 52 | } |
53 | 53 | } |
54 | 54 | ---- |
55 | | -==== |
56 | 55 |
|
57 | | -The `Customer_` type is a metamodel type generated using the JPA Metamodel generator (see the link:$$https://docs.jboss.org/hibernate/jpamodelgen/1.0/reference/en-US/html_single/#whatisit$$[Hibernate implementation's documentation for an example]). |
| 56 | +The `Customer_` type is a metamodel type generated using the JPA Metamodel generator (see the link:$$https://docs.jboss.org/hibernate/jpamodelgen/1.3/reference/en-US/html_single/#whatisit$$[Hibernate implementation's documentation for an example]). |
58 | 57 | So the expression, `Customer_.createdAt`, assumes the `Customer` has a `createdAt` attribute of type `Date`. |
59 | 58 | Besides that, we have expressed some criteria on a business requirement abstraction level and created executable `Specifications`. |
60 | | -So a client might use a `Specification` as follows: |
61 | | - |
62 | | -.Using a simple Specification |
63 | 59 | ==== |
64 | | -[source, java] |
| 60 | + |
| 61 | +Use a specification directly with a repository: |
| 62 | + |
| 63 | +[source,java] |
65 | 64 | ---- |
66 | 65 | List<Customer> customers = customerRepository.findAll(isLongTermCustomer()); |
67 | 66 | ---- |
68 | | -==== |
69 | 67 |
|
70 | | -Why not create a query for this kind of data access? Using a single `Specification` does not gain a lot of benefit over a plain query declaration. The power of specifications really shines when you combine them to create new `Specification` objects. You can achieve this through the default methods of `Specification` we provide to build expressions similar to the following: |
| 68 | +Specifications become most valuable when composed: |
71 | 69 |
|
72 | | -.Combined Specifications |
73 | | -==== |
74 | | -[source, java] |
| 70 | +[source,java] |
75 | 71 | ---- |
76 | 72 | MonetaryAmount amount = new MonetaryAmount(200.0, Currencies.DOLLAR); |
77 | 73 | List<Customer> customers = customerRepository.findAll( |
78 | | - isLongTermCustomer().or(hasSalesOfMoreThan(amount))); |
| 74 | + isLongTermCustomer().or(hasSalesOfMoreThan(amount)) |
| 75 | +); |
79 | 76 | ---- |
80 | 77 |
|
81 | | -`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. |
| 78 | +[[specification-interfaces]] |
| 79 | +== `Specification`, `UpdateSpecification`, `DeleteSpecification` |
| 80 | + |
| 81 | +The javadoc:org.springframework.data.jpa.domain.Specification[] interface has been available for a much longer time and is tied a particular query type (select, update, delete) as per Criteria API restrictions. |
| 82 | +The three specification interfaces are defined as follows: |
| 83 | + |
| 84 | +[tabs] |
| 85 | +====== |
| 86 | +Specification:: |
| 87 | ++ |
| 88 | +==== |
| 89 | +[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
| 90 | +---- |
| 91 | +public interface Specification<T> { |
| 92 | + Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, |
| 93 | + CriteriaBuilder builder); |
| 94 | +} |
| 95 | +---- |
82 | 96 | ==== |
83 | 97 |
|
84 | | -And with JPA 2.1, the `CriteriaBuilder` API introduced `CriteriaDelete`. This is provided through `JpaSpecificationExecutor`'s `delete(Specification)` API. |
| 98 | +UpdateSpecification:: |
| 99 | ++ |
| 100 | +==== |
| 101 | +[source,java,indent=0,subs="verbatim,quotes",role="secondary"] |
| 102 | +---- |
| 103 | +public interface UpdateSpecification<T> { |
| 104 | + Predicate toPredicate(Root<T> root, CriteriaUpdate<T> update, |
| 105 | + CriteriaBuilder builder); |
| 106 | +} |
| 107 | +---- |
| 108 | +==== |
85 | 109 |
|
86 | | -.Using a `Specification` to delete entries. |
| 110 | +DeleteSpecification:: |
| 111 | ++ |
| 112 | +==== |
| 113 | +[source,java,indent=0,subs="verbatim,quotes",role="tertiary"] |
| 114 | +---- |
| 115 | +public interface DeleteSpecification<T> { |
| 116 | + Predicate toPredicate(Root<T> root, CriteriaDelete<T> delete, |
| 117 | + CriteriaBuilder builder); |
| 118 | +} |
| 119 | +---- |
| 120 | +==== |
| 121 | +====== |
| 122 | + |
| 123 | +`Specification` objects can be constructed either directly or by reusing `PredicateSpecification` instances, as shown in the following example: |
| 124 | + |
| 125 | +.Specifications for a Customer |
87 | 126 | ==== |
88 | 127 | [source, java] |
89 | 128 | ---- |
90 | | -Specification<User> ageLessThan18 = (root, query, cb) -> cb.lessThan(root.get("age").as(Integer.class), 18) |
| 129 | +public class CustomerSpecs { |
91 | 130 |
|
92 | | -userRepository.delete(ageLessThan18); |
| 131 | + public static UpdateSpecification<Customer> updateLastnameByFirstnameAndLastname(String newLastName, String currentFirstname, String currentLastname) { |
| 132 | + return UpdateSpecification<User> updateLastname = UpdateSpecification.<User> update((root, update, criteriaBuilder) -> { |
| 133 | + update.set("lastname", newLastName); |
| 134 | + }).where(hasFirstname(currentFirstname).and(hasLastname(currentLastname))); |
| 135 | + } |
| 136 | +
|
| 137 | + public static PredicateSpecification<Customer> hasFirstname(String firstname) { |
| 138 | + return (root, builder) -> { |
| 139 | + return builder.equal(from.get("firstname"), value); |
| 140 | + }; |
| 141 | + } |
| 142 | +
|
| 143 | + public static PredicateSpecification<Customer> hasLastname(String lastname) { |
| 144 | + return (root, builder) -> { |
| 145 | + // build query here |
| 146 | + }; |
| 147 | + } |
| 148 | +} |
93 | 149 | ---- |
94 | | -The `Specification` builds up a criteria where the `age` field (cast as an integer) is less than `18`. |
95 | | -Passed on to the `userRepository`, it will use JPA's `CriteriaDelete` feature to generate the right `DELETE` operation. |
96 | | -It then returns the number of entities deleted. |
97 | 150 | ==== |
98 | 151 |
|
| 152 | +[[specification-fluent]] |
| 153 | +== Fluent API |
99 | 154 |
|
| 155 | +`JpaSpecificationExecutor` defines fluent query methods for flexible execution of queries based on `Specification` instances: |
| 156 | + |
| 157 | +1. For `PredicateSpecification`: `findBy(PredicateSpecification<T> spec, Function<? super SpecificationFluentQuery<S>, R> queryFunction)` |
| 158 | +2. For `Specification`: `findBy(Specification<T> spec, Function<? super SpecificationFluentQuery<S>, R> queryFunction)` |
| 159 | + |
| 160 | +As with other methods, it executes a query derived from a `Specification`. |
| 161 | +However, the query function allows you to take control over aspects of query execution that you cannot dynamically control otherwise. |
| 162 | +You do so by invoking the various intermediate and terminal methods of `SpecificationFluentQuery`. |
| 163 | + |
| 164 | +**Intermediate methods** |
| 165 | + |
| 166 | +* `sortBy`: Apply an ordering for your result. |
| 167 | +Repeated method calls append each `Sort` (note that `page(Pageable)` using a sorted `Pageable` overrides any previous sort order). |
| 168 | +* `limit`: Limit the result count. |
| 169 | +* `as`: Specify the type to be read or projected to. |
| 170 | +* `project`: Limit the queries properties. |
| 171 | + |
| 172 | +**Terminal methods** |
| 173 | + |
| 174 | +* `first`, `firstValue`: Return the first value. `first` returns an `Optional<T>` or `Optional.empty()` if the query did not yield any result. `firstValue` is its nullable variant without the need to use `Optional`. |
| 175 | +* `one`, `oneValue`: Return the one value. `one` returns an `Optional<T>` or `Optional.empty()` if the query did not yield any result. `oneValue` is its nullable variant without the need to use `Optional`. |
| 176 | +Throws `IncorrectResultSizeDataAccessException` if more than one match found. |
| 177 | +* `all`: Return all results as a `List<T>`. |
| 178 | +* `page(Pageable)`: Return all results as a `Page<T>`. |
| 179 | +* `slice(Pageable)`: Return all results as a `Slice<T>`. |
| 180 | +* `scroll(ScrollPosition)`: Use scrolling (offset, keyset) to retrieve results as a `Window<T>`. |
| 181 | +* `stream()`: Return a `Stream<T>` to process results lazily. |
| 182 | +The stream is stateful and must be closed after use. |
| 183 | +* `count` and `exists`: Return the count of matching entities or whether any match exists. |
| 184 | + |
| 185 | +NOTE: Intermediate and terminal methods must be invoked within the query function. |
| 186 | + |
| 187 | +.Use the fluent API to get a projected `Page`, ordered by `lastname` |
| 188 | +==== |
| 189 | +[source,java] |
| 190 | +---- |
| 191 | +Page<CustomerProjection> page = repository.findBy(spec, |
| 192 | + q -> q.as(CustomerProjection.class) |
| 193 | + .page(PageRequest.of(0, 20, Sort.by("lastname"))) |
| 194 | +); |
| 195 | +---- |
| 196 | +==== |
| 197 | + |
| 198 | +.Use the fluent API to get the last of potentially many results, ordered by `lastname` |
| 199 | +==== |
| 200 | +[source,java] |
| 201 | +---- |
| 202 | +Optional<Customer> match = repository.findBy(spec, |
| 203 | + q -> q.sortBy(Sort.by("lastname").descending()) |
| 204 | + .first() |
| 205 | +); |
| 206 | +---- |
| 207 | +==== |
0 commit comments