Skip to content

Commit 32a4286

Browse files
committed
Document projection support for JPASpecificationExecutor.
See #2499
1 parent 05aff85 commit 32a4286

File tree

1 file changed

+151
-43
lines changed

1 file changed

+151
-43
lines changed
Lines changed: 151 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,99 +1,207 @@
11
[[specifications]]
22
= Specifications
33

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:
78

89
[source, java]
910
----
1011
public interface CustomerRepository extends CrudRepository<Customer, Long>, JpaSpecificationExecutor<Customer> {
11-
1212
}
1313
----
1414

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:
1617

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
2123

22-
The `Specification` interface is defined as follows:
24+
The `PredicateSpecification` interface is defined with a minimal set of dependencies allowing broad functional composition:
2325

2426
[source, java]
2527
----
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);
2930
}
3031
----
3132

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:
3334

3435
.Specifications for a Customer
3536
====
36-
[source, java]
37+
[source,java]
3738
----
38-
public class CustomerSpecs {
39-
39+
class CustomerSpecs {
4040
41-
public static Specification<Customer> isLongTermCustomer() {
42-
return (root, query, builder) -> {
41+
static PredicateSpecification<Customer> isLongTermCustomer() {
42+
return (from, builder) -> {
4343
LocalDate date = LocalDate.now().minusYears(2);
44-
return builder.lessThan(root.get(Customer_.createdAt), date);
44+
return builder.lessThan(from.get(Customer_.createdAt), date);
4545
};
4646
}
4747
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
5151
};
5252
}
5353
}
5454
----
55-
====
5655
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]).
5857
So the expression, `Customer_.createdAt`, assumes the `Customer` has a `createdAt` attribute of type `Date`.
5958
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
6359
====
64-
[source, java]
60+
61+
Use a specification directly with a repository:
62+
63+
[source,java]
6564
----
6665
List<Customer> customers = customerRepository.findAll(isLongTermCustomer());
6766
----
68-
====
6967

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:
7169

72-
.Combined Specifications
73-
====
74-
[source, java]
70+
[source,java]
7571
----
7672
MonetaryAmount amount = new MonetaryAmount(200.0, Currencies.DOLLAR);
7773
List<Customer> customers = customerRepository.findAll(
78-
isLongTermCustomer().or(hasSalesOfMoreThan(amount)));
74+
isLongTermCustomer().or(hasSalesOfMoreThan(amount))
75+
);
7976
----
8077

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+
----
8296
====
8397
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+
====
85109
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
87126
====
88127
[source, java]
89128
----
90-
Specification<User> ageLessThan18 = (root, query, cb) -> cb.lessThan(root.get("age").as(Integer.class), 18)
129+
public class CustomerSpecs {
91130
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+
}
93149
----
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.
97150
====
98151

152+
[[specification-fluent]]
153+
== Fluent API
99154

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

Comments
 (0)