Skip to content

Commit 75f2719

Browse files
mp911deodrotbohm
authored andcommitted
DATAJPA-218 - Further work on Query by Example.
Add reference documentation. Adapted to API changes in Spring Data Commons. Related tickets: DATACMNS-810. Original pull request: #164.
1 parent 88abb0d commit 75f2719

File tree

12 files changed

+453
-146
lines changed

12 files changed

+453
-146
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
target/
2+
.idea/
23
.settings/
4+
*.iml
35
.project
46
.classpath
57
.springBeans

src/main/asciidoc/index.adoc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
= Spring Data JPA - Reference Documentation
2-
Oliver Gierke; Thomas Darimont; Christoph Strobl
2+
Oliver Gierke; Thomas Darimont; Christoph Strobl; Mark Paluch
33
:revnumber: {version}
44
:revdate: {localdate}
55
:toc:
66
:toc-placement!:
77
:spring-data-commons-docs: ../../../../spring-data-commons/src/main/asciidoc
88
:spring-framework-docs: http://docs.spring.io/spring-framework/docs/current/spring-framework-reference/html
99

10-
(C) 2008-2015 The original authors.
10+
(C) 2008-2016 The original authors.
1111

1212
NOTE: Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically.
1313

src/main/asciidoc/jpa.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,9 @@ List<Customer> customers = customerRepository.findAll(
604604
As you can see, `Specifications` offers some glue-code methods to chain and combine `Specification` instances. Thus extending your data access layer is just a matter of creating new `Specification` implementations and combining them with ones already existing.
605605
====
606606

607+
include::{spring-data-commons-docs}/query-by-example.adoc[]
608+
include::query-by-example.adoc[]
609+
607610
[[transactions]]
608611
== Transactionality
609612
CRUD methods on repository instances are transactional by default. For reading operations the transaction configuration `readOnly` flag is set to true, all others are configured with a plain `@Transactional` so that default transaction configuration applies. For details see JavaDoc of `CrudRepository`. If you need to tweak transaction configuration for one of the methods declared in a repository simply redeclare the method in your repository interface as follows:
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
[[query.by.example.execution]]
2+
== Executing Example
3+
4+
In Spring Data JPA you can use Query by Example with Repositories.
5+
6+
.Query by Example using a Repository
7+
====
8+
[source, java]
9+
----
10+
public interface PersonRepository extends JpaRepository<Person, String> {
11+
12+
}
13+
14+
public class PersonService {
15+
16+
@Autowired PersonRepository personRepository;
17+
18+
public List<Person> findPeople(Person probe) {
19+
return personRepository.findAll(Example.of(probe));
20+
}
21+
}
22+
----
23+
====
24+
25+
An `Example` containing an untyped `ExampleSpec` uses the Repository type. Typed `ExampleSpec` use their type for creating JPA queries.
26+
27+
NOTE: Only SingularAttribute properties can be used for property matching.
28+
29+
30+
Property specifier accepts property names (e.g. "firstname" and "lastname"). You can navigate by chaining properties together with dots ("address.city"). You can tune it with matching options and case sensitivity.
31+
32+
[cols="1,2", options="header"]
33+
.`StringMatcher` options
34+
|===
35+
| Matching
36+
| Logical result
37+
38+
| `DEFAULT` (case-sensitive)
39+
| `firstname = ?0`
40+
41+
| `DEFAULT` (case-insensitive)
42+
| `LOWER(firstname) = LOWER(?0)`
43+
44+
| `EXACT` (case-sensitive)
45+
| `firstname = ?0`
46+
47+
| `EXACT` (case-insensitive)
48+
| `LOWER(firstname) = LOWER(?0)`
49+
50+
| `STARTING` (case-sensitive)
51+
| `firstname like ?0 + '%'`
52+
53+
| `STARTING` (case-insensitive)
54+
| `LOWER(firstname) like LOWER(?0) + '%'`
55+
56+
| `ENDING` (case-sensitive)
57+
| `firstname like '%' + ?0`
58+
59+
| `ENDING` (case-insensitive)
60+
| `LOWER(firstname) like '%' + LOWER(?0)`
61+
62+
| `CONTAINING` (case-sensitive)
63+
| `firstname like '%' + ?0 + '%'`
64+
65+
| `CONTAINING` (case-insensitive)
66+
| `LOWER(firstname) like '%' + LOWER(?0) + '%'`
67+
68+
|===

src/main/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilder.java

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434

3535
import org.springframework.dao.InvalidDataAccessApiUsageException;
3636
import org.springframework.data.domain.Example;
37-
import org.springframework.data.domain.Example.NullHandler;
37+
import org.springframework.data.domain.ExampleSpec;
38+
import org.springframework.data.repository.core.support.ExampleSpecAccessor;
3839
import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper;
3940
import org.springframework.orm.jpa.JpaSystemException;
4041
import org.springframework.util.Assert;
@@ -48,8 +49,9 @@
4849
* The builder includes any {@link SingularAttribute} of the {@link Example#getProbe()} applying {@link String} and
4950
* {@literal null} matching strategies configured on the {@link Example}. Ignored paths are no matter of their actual
5051
* value not considered. <br />
51-
*
52+
*
5253
* @author Christoph Strobl
54+
* @author Mark Paluch
5355
* @since 1.10
5456
*/
5557
public class QueryByExamplePredicateBuilder {
@@ -63,7 +65,7 @@ public class QueryByExamplePredicateBuilder {
6365

6466
/**
6567
* Extract the {@link Predicate} representing the {@link Example}.
66-
*
68+
*
6769
* @param root must not be {@literal null}.
6870
* @param cb must not be {@literal null}.
6971
* @param example must not be {@literal null}.
@@ -73,13 +75,14 @@ public static <T> Predicate getPredicate(Root<T> root, CriteriaBuilder cb, Examp
7375

7476
Assert.notNull(root, "Root must not be null!");
7577
Assert.notNull(cb, "CriteriaBuilder must not be null!");
76-
Assert.notNull(example, "Root must not be null!");
78+
Assert.notNull(example, "Example must not be null!");
7779

78-
List<Predicate> predicates = getPredicates("", cb, root, root.getModel(), example.getSampleObject(), example,
79-
new PathNode("root", null, example.getSampleObject()));
80+
List<Predicate> predicates = getPredicates("", cb, root, root.getModel(), example.getProbe(),
81+
example.getProbeType(), new ExampleSpecAccessor(example.getExampleSpec()),
82+
new PathNode("root", null, example.getProbe()));
8083

8184
if (predicates.isEmpty()) {
82-
return cb.isTrue(cb.literal(false));
85+
return cb.isTrue(cb.literal(true));
8386
}
8487

8588
if (predicates.size() == 1) {
@@ -90,8 +93,8 @@ public static <T> Predicate getPredicate(Root<T> root, CriteriaBuilder cb, Examp
9093
}
9194

9295
@SuppressWarnings({ "rawtypes", "unchecked" })
93-
static List<Predicate> getPredicates(String path, CriteriaBuilder cb, Path<?> from, ManagedType<?> type,
94-
Object value, Example<?> example, PathNode currentNode) {
96+
static List<Predicate> getPredicates(String path, CriteriaBuilder cb, Path<?> from, ManagedType<?> type, Object value,
97+
Class<?> probeType, ExampleSpecAccessor exampleAccessor, PathNode currentNode) {
9598

9699
List<Predicate> predicates = new ArrayList<Predicate>();
97100
DirectFieldAccessFallbackBeanWrapper beanWrapper = new DirectFieldAccessFallbackBeanWrapper(value);
@@ -100,16 +103,16 @@ static List<Predicate> getPredicates(String path, CriteriaBuilder cb, Path<?> fr
100103

101104
String currentPath = !StringUtils.hasText(path) ? attribute.getName() : path + "." + attribute.getName();
102105

103-
if (example.isIgnoredPath(currentPath)) {
106+
if (exampleAccessor.isIgnoredPath(currentPath)) {
104107
continue;
105108
}
106109

107-
Object attributeValue = example.getValueTransformerForPath(currentPath).convert(
108-
beanWrapper.getPropertyValue(attribute.getName()));
110+
Object attributeValue = exampleAccessor.getValueTransformerForPath(currentPath)
111+
.convert(beanWrapper.getPropertyValue(attribute.getName()));
109112

110113
if (attributeValue == null) {
111114

112-
if (example.getNullHandler().equals(NullHandler.INCLUDE)) {
115+
if (exampleAccessor.getNullHandler().equals(ExampleSpec.NullHandler.INCLUDE)) {
113116
predicates.add(cb.isNull(from.get(attribute)));
114117
}
115118
continue;
@@ -118,39 +121,39 @@ static List<Predicate> getPredicates(String path, CriteriaBuilder cb, Path<?> fr
118121
if (attribute.getPersistentAttributeType().equals(PersistentAttributeType.EMBEDDED)) {
119122

120123
predicates.addAll(getPredicates(currentPath, cb, from.get(attribute.getName()),
121-
(ManagedType<?>) attribute.getType(), attributeValue, example, currentNode));
124+
(ManagedType<?>) attribute.getType(), attributeValue, probeType, exampleAccessor, currentNode));
122125
continue;
123126
}
124127

125128
if (isAssociation(attribute)) {
126129

127130
if (!(from instanceof From)) {
128-
throw new JpaSystemException(new IllegalArgumentException(String.format(
129-
"Unexpected path type for %s. Found % where From.class was expected.", currentPath, from)));
131+
throw new JpaSystemException(new IllegalArgumentException(
132+
String.format("Unexpected path type for %s. Found % where From.class was expected.", currentPath, from)));
130133
}
131134

132135
PathNode node = currentNode.add(attribute.getName(), attributeValue);
133136
if (node.spansCycle()) {
134-
throw new InvalidDataAccessApiUsageException(String.format(
135-
"Path '%s' from root %s must not span a cyclic property reference!\r\n%s", currentPath,
136-
ClassUtils.getShortName(example.getSampleType()), node));
137+
throw new InvalidDataAccessApiUsageException(
138+
String.format("Path '%s' from root %s must not span a cyclic property reference!\r\n%s", currentPath,
139+
ClassUtils.getShortName(probeType), node));
137140
}
138141

139142
predicates.addAll(getPredicates(currentPath, cb, ((From<?, ?>) from).join(attribute.getName()),
140-
(ManagedType<?>) attribute.getType(), attributeValue, example, node));
143+
(ManagedType<?>) attribute.getType(), attributeValue, probeType, exampleAccessor, node));
141144

142145
continue;
143146
}
144147

145148
if (attribute.getJavaType().equals(String.class)) {
146149

147150
Expression<String> expression = from.get(attribute);
148-
if (example.isIgnoreCaseForPath(currentPath)) {
151+
if (exampleAccessor.isIgnoreCaseForPath(currentPath)) {
149152
expression = cb.lower(expression);
150153
attributeValue = attributeValue.toString().toLowerCase();
151154
}
152155

153-
switch (example.getStringMatcherForPath(currentPath)) {
156+
switch (exampleAccessor.getStringMatcherForPath(currentPath)) {
154157

155158
case DEFAULT:
156159
case EXACT:
@@ -166,8 +169,8 @@ static List<Predicate> getPredicates(String path, CriteriaBuilder cb, Path<?> fr
166169
predicates.add(cb.like(expression, "%" + attributeValue));
167170
break;
168171
default:
169-
throw new IllegalArgumentException("Unsupported StringMatcher "
170-
+ example.getStringMatcherForPath(currentPath));
172+
throw new IllegalArgumentException(
173+
"Unsupported StringMatcher " + exampleAccessor.getStringMatcherForPath(currentPath));
171174
}
172175
} else {
173176
predicates.add(cb.equal(from.get(attribute), attributeValue));
@@ -184,7 +187,7 @@ private static boolean isAssociation(Attribute<?, ?> attribute) {
184187
/**
185188
* {@link PathNode} is used to dynamically grow a directed graph structure that allows to detect cycles within its
186189
* direct predecessor nodes by comparing parent node values using {@link System#identityHashCode(Object)}.
187-
*
190+
*
188191
* @author Christoph Strobl
189192
*/
190193
private static class PathNode {

src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
* @author Oliver Gierke
5353
* @author Thomas Darimont
5454
*/
55-
public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor {
55+
public enum PersistenceProvider implements QueryExtractor,ProxyIdAccessor {
5656

5757
/**
5858
* Hibernate persistence provider.
@@ -117,14 +117,13 @@ public <T> Collection<T> potentiallyConvertEmptyCollection(Collection<T> collect
117117
public CloseableIterator<Object> executeQueryWithResultStream(Query jpaQuery) {
118118
return new HibernateScrollableResultsIterator<Object>(jpaQuery);
119119
}
120-
121120
},
122121

123122
/**
124123
* EclipseLink persistence provider.
125124
*/
126-
ECLIPSELINK(Collections.singleton(ECLIPSELINK_ENTITY_MANAGER_INTERFACE), Collections
127-
.singleton(ECLIPSELINK_JPA_METAMODEL_TYPE)) {
125+
ECLIPSELINK(Collections.singleton(ECLIPSELINK_ENTITY_MANAGER_INTERFACE),
126+
Collections.singleton(ECLIPSELINK_JPA_METAMODEL_TYPE)) {
128127

129128
public String extractQueryString(Query query) {
130129
return ((JpaQuery<?>) query).getDatabaseQuery().getJPQLString();
@@ -164,7 +163,6 @@ public <T> Collection<T> potentiallyConvertEmptyCollection(Collection<T> collect
164163
public CloseableIterator<Object> executeQueryWithResultStream(Query jpaQuery) {
165164
return new EclipseLinkScrollableResultsIterator<Object>(jpaQuery);
166165
}
167-
168166
},
169167

170168
/**
@@ -203,7 +201,6 @@ public Object getIdentifierFrom(Object entity) {
203201
public CloseableIterator<Object> executeQueryWithResultStream(Query jpaQuery) {
204202
return new OpenJpaResultStreamingIterator<Object>(jpaQuery);
205203
}
206-
207204
},
208205

209206
/**
@@ -246,7 +243,6 @@ public boolean shouldUseAccessorFor(Object entity) {
246243
public Object getIdentifierFrom(Object entity) {
247244
return null;
248245
}
249-
250246
};
251247

252248
/**
@@ -388,8 +384,8 @@ public <T> Collection<T> potentiallyConvertEmptyCollection(Collection<T> collect
388384
}
389385

390386
public CloseableIterator<Object> executeQueryWithResultStream(Query jpaQuery) {
391-
throw new UnsupportedOperationException("Streaming results is not implement for this PersistenceProvider: "
392-
+ name());
387+
throw new UnsupportedOperationException(
388+
"Streaming results is not implement for this PersistenceProvider: " + name());
393389
}
394390

395391
/**

src/main/java/org/springframework/data/jpa/repository/JpaRepository.java

Lines changed: 13 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,21 @@
2121
import javax.persistence.EntityManager;
2222

2323
import org.springframework.data.domain.Example;
24-
import org.springframework.data.domain.Page;
25-
import org.springframework.data.domain.Pageable;
2624
import org.springframework.data.domain.Sort;
2725
import org.springframework.data.repository.NoRepositoryBean;
2826
import org.springframework.data.repository.PagingAndSortingRepository;
27+
import org.springframework.data.repository.query.QueryByExampleExecutor;
2928

3029
/**
3130
* JPA specific extension of {@link org.springframework.data.repository.Repository}.
32-
*
31+
*
3332
* @author Oliver Gierke
3433
* @author Christoph Strobl
34+
* @author Mark Paluch
3535
*/
3636
@NoRepositoryBean
37-
public interface JpaRepository<T, ID extends Serializable> extends PagingAndSortingRepository<T, ID> {
37+
public interface JpaRepository<T, ID extends Serializable>
38+
extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
3839

3940
/*
4041
* (non-Javadoc)
@@ -95,33 +96,16 @@ public interface JpaRepository<T, ID extends Serializable> extends PagingAndSort
9596
*/
9697
T getOne(ID id);
9798

98-
/**
99-
* Returns all instances of the type specified by the given {@link Example}.
100-
*
101-
* @param example must not be {@literal null}.
102-
* @return
103-
* @since 1.10
99+
/* (non-Javadoc)
100+
* @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example)
104101
*/
105-
List<T> findAllByExample(Example<T> example);
102+
@Override
103+
<S extends T> List<S> findAll(Example<S> example);
106104

107-
/**
108-
* Returns all instances of the type specified by the given {@link Example}.
109-
*
110-
* @param example must not be {@literal null}.
111-
* @param sort can be {@literal null}.
112-
* @return all entities sorted by the given options
113-
* @since 1.10
105+
/* (non-Javadoc)
106+
* @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example, org.springframework.data.domain.Sort)
114107
*/
115-
List<T> findAllByExample(Example<T> example, Sort sort);
108+
@Override
109+
<S extends T> List<S> findAll(Example<S> example, Sort sort);
116110

117-
/**
118-
* Returns a {@link Page} of entities meeting the paging restriction specified by the given {@link Example} limited to
119-
* criteria provided in the {@code Pageable} object.
120-
*
121-
* @param example must not be {@literal null}.
122-
* @param pageable can be {@literal null}.
123-
* @return a {@link Page} of entities
124-
* @since 1.10
125-
*/
126-
Page<T> findAllByExample(Example<T> example, Pageable pageable);
127111
}

src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,8 @@ private Query applyEntityGraphConfiguration(Query query, JpaQueryMethod method)
189189
Assert.notNull(query, "Query must not be null!");
190190
Assert.notNull(method, "JpaQueryMethod must not be null!");
191191

192-
Map<String, Object> hints = Jpa21Utils.tryGetFetchGraphHints(em, method.getEntityGraph(), getQueryMethod()
193-
.getEntityInformation().getJavaType());
192+
Map<String, Object> hints = Jpa21Utils.tryGetFetchGraphHints(em, method.getEntityGraph(),
193+
getQueryMethod().getEntityInformation().getJavaType());
194194

195195
for (Map.Entry<String, Object> hint : hints.entrySet()) {
196196
query.setHint(hint.getKey(), hint.getValue());

0 commit comments

Comments
 (0)