Skip to content

Commit 26f725b

Browse files
committed
DATACMNS-810 - Polishing.
Renamed ExampleSpecification to ExampleMatcher and introduced a matching() factory method, dropping the dedicated override mechanism for types for now. Updated documentation accordingly. Used Lombok for constructor creation, field defaults, toString() as well as equals(…) and hashCode() implementations. Tweaked imports in unit tests. Renamed methods to express expected behavior. Original pull request: #153.
1 parent b0ce700 commit 26f725b

14 files changed

+806
-1869
lines changed

lombok.config

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
lombok.nonNull.exceptionType = IllegalArgumentException
2+
lombok.log.fieldName = LOG

src/main/asciidoc/query-by-example.adoc

+32-80
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ Query by Example (QBE) is a user-friendly querying technique with a simple inter
1212
The Query by Example API consists of three parts:
1313

1414
* Probe: That is the actual example of a domain object with populated fields.
15-
* `ExampleSpec`: The `ExampleSpec` carries details on how to match particular fields. It can be reused across multiple Examples. `ExampleSpec` comes in two flavors: <<query.by.example.examplespec,untyped>> and <<query.by.example.examplespec.typed,typed>>.
16-
* `Example`: An Example consists of the probe and the ExampleSpec. It is used to create the query. An `Example` takes a probe (usually the domain object or a subtype of it) and the `ExampleSpec`.
15+
* `ExampleMatcher`: The `ExampleMatcher` carries details on how to match particular fields. It can be reused across multiple Examples.
16+
* `Example`: An `Example` consists of the probe and the `ExampleMatcher`. It is used to create the query.
1717

1818
Query by Example is suited for several use-cases but also comes with limitations:
1919

@@ -48,14 +48,13 @@ public class Person {
4848
----
4949
====
5050

51-
This is a simple domain object. You can use it to create an `Example`. By default, fields having `null` values are ignored, and strings are matched using the store specific defaults. Examples can be built by either using the `of` factory method or by using <<query.by.example.examplespec,`ExampleSpec`>>. `Example` is immutable.
51+
This is a simple domain object. You can use it to create an `Example`. By default, fields having `null` values are ignored, and strings are matched using the store specific defaults. Examples can be built by either using the `of` factory method or by using <<query.by.example.matcher,`ExampleMatcher`>>. `Example` is immutable.
5252

5353
.Simple Example
5454
====
5555
[source,java]
5656
----
5757
Person person = new Person(); <1>
58-
5958
person.setFirstname("Dave"); <2>
6059
6160
Example<Person> example = Example.of(person); <3>
@@ -65,19 +64,17 @@ Example<Person> example = Example.of(person); <3>
6564
<3> Create the `Example`
6665
====
6766

68-
NOTE: Property names of the sample object must correlate with the property names of the queried domain object.
69-
70-
Examples can be executed ideally with Repositories. To do so, let your repository extend from `QueryByExampleExecutor`, here's an excerpt from the `QueryByExampleExecutor` interface:
67+
Examples are ideally be executed with repositories. To do so, let your repository interface extend `QueryByExampleExecutor<T>`. Here's an excerpt from the `QueryByExampleExecutor` interface:
7168

7269
.The `QueryByExampleExecutor`
7370
====
7471
[source, java]
7572
----
7673
public interface QueryByExampleExecutor<T> {
7774
78-
<S extends T> S findOne(Example<S> example);
75+
<S extends T> S findOne(Example<S> example);
7976
80-
<S extends T> Iterable<S> findAll(Example<S> example);
77+
<S extends T> Iterable<S> findAll(Example<S> example);
8178
8279
// … more functionality omitted.
8380
}
@@ -86,50 +83,44 @@ public interface QueryByExampleExecutor<T> {
8683

8784
You can read more about <<query.by.example.execution, Query by Example Execution>> below.
8885

89-
[[query.by.example.examplespec]]
90-
=== Example Spec
86+
[[query.by.example.matcher]]
87+
=== Example matchers
9188

92-
Examples are not limited to default settings. You can specify own defaults for string matching, null handling and property-specific settings using the `ExampleSpec`. `ExampleSpec` comes in two flavors: untyped and typed. By default `Example.of(Person.class)` uses an untyped `ExampleSpec`. Using untyped `ExampleSpec` will use the Repository entity information to determine the type to query and has no control over inheritance queries. Also, untyped `ExampleSpec` will use the probe type when using with a Template to determine the type to query. Read more about <<query.by.example.examplespec.typed,typed `ExampleSpec`>> below.
89+
Examples are not limited to default settings. You can specify own defaults for string matching, null handling and property-specific settings using the `ExampleMatcher`. `ExampleMatcher` comes in two flavors: untyped and typed. By default `Example.of(Person.class)` uses an untyped `ExampleMatcher`. Using untyped `ExampleMatcher` will use the Repository entity information to determine the type to query and has no control over inheritance queries. Also, untyped `ExampleMatcher` will use the probe type when using with a Template to determine the type to query. Read more about <<query.by.example.matcher.typed,typed `ExampleMatcher`>> below.
9390

9491
.Untyped Example Spec with customized matching
9592
====
9693
[source,java]
9794
----
98-
Person person = new Person(); <1>
99-
100-
person.setFirstname("Dave"); <2>
101-
102-
ExampleSpec exampleSpec = ExampleSpec.untyped() <3>
95+
Person person = new Person(); <1>
96+
person.setFirstname("Dave"); <2>
10397
104-
.withIgnorePaths("lastname") <4>
98+
ExampleMatcher matcher = ExampleMatcher.matching() <3>
99+
.withIgnorePaths("lastname") <4>
100+
.withIncludeNullValues() <5>
101+
.withStringMatcherEnding(); <6>
105102
106-
.withIncludeNullValues() <5>
107-
108-
.withStringMatcherEnding(); <6>
109-
110-
Example<Person> example = Example.of(person, exampleSpec); <7>
103+
Example<Person> example = Example.of(person, matcher); <7>
111104
112105
----
113106
<1> Create a new instance of the domain object.
114107
<2> Set properties.
115-
<3> Create an untyped `ExampleSpec`. The `ExampleSpec` is usable at this stage.
116-
<4> Construct a new `ExampleSpec` to ignore the property path `lastname`.
117-
<5> Construct a new `ExampleSpec` to ignore the property path `lastname` and to include null values.
118-
<6> Construct a new `ExampleSpec` to ignore the property path `lastname`, to include null values, and use perform suffix string matching.
119-
<7> Create a new `Example` based on the domain object and the configured `ExampleSpec`.
108+
<3> Create an `ExampleMatcher` which is usable at this stage even without further configuration.
109+
<4> Construct a new `ExampleMatcher` to ignore the property path `lastname`.
110+
<5> Construct a new `ExampleMatcher` to ignore the property path `lastname` and to include null values.
111+
<6> Construct a new `ExampleMatcher` to ignore the property path `lastname`, to include null values, and use perform suffix string matching.
112+
<7> Create a new `Example` based on the domain object and the configured `ExampleMatcher`.
120113
====
121114

122-
`ExampleSpec` is immutable. Calls to `with…(…)` return a copy of `ExampleSpec` with the specific setting applied. Intermediate objects can be safely reused. An `ExampleSpec` can be used to create ad-hoc example specs or to be reused across the application as a specification for `Example`. You can use `ExampleSpec` as a template to configure a default behavior for your example spec. You also can derive from it a more specific `ExampleSpec` where you need to customize it.
123-
124115
You can specify behavior for individual properties (e.g. "firstname" and "lastname", "address.city" for nested properties). You can tune it with matching options and case sensitivity.
125116

126117
.Configuring matcher options
127118
====
128119
[source,java]
129120
----
130-
ExampleSpec exampleSpec = ExampleSpec.untyped()
131-
.withMatcher("firstname", endsWith())
132-
.withMatcher("lastname", startsWith().ignoreCase());
121+
ExampleMatcher exampleSpec = ExampleMatcher.untyped()
122+
.withMatcher("firstname", endsWith())
123+
.withMatcher("lastname", startsWith().ignoreCase());
133124
}
134125
----
135126
====
@@ -140,74 +131,35 @@ Another style to configure matcher options is by using Java 8 lambdas. This appr
140131
====
141132
[source,java]
142133
----
143-
ExampleSpec exampleSpec = ExampleSpec.untyped()
144-
.withMatcher("firstname", matcher -> matcher.endsWith())
145-
.withMatcher("firstname", matcher -> matcher.startsWith());
134+
ExampleMatcher exampleSpec = ExampleMatcher.untyped()
135+
.withMatcher("firstname", matcher -> matcher.endsWith())
136+
.withMatcher("firstname", matcher -> matcher.startsWith());
146137
}
147138
----
148139
====
149140

150-
Queries created by `Example` use a merged view of the configuration. Default matching settings can be set at `ExampleSpec` level while individual settings can be applied to particular property paths. Settings that are set on `ExampleSpec` are inherited by property path settings unless they are defined explicitly. Settings on a property patch have higher precedence than default settings.
141+
Queries created by `Example` use a merged view of the configuration. Default matching settings can be set at `ExampleMatcher` level while individual settings can be applied to particular property paths. Settings that are set on `ExampleMatcher` are inherited by property path settings unless they are defined explicitly. Settings on a property patch have higher precedence than default settings.
151142

152143
[cols="1,2", options="header"]
153-
.Scope of `ExampleSpec` settings
144+
.Scope of `ExampleMatcher` settings
154145
|===
155146
| Setting
156147
| Scope
157148

158149
| Null-handling
159-
| `ExampleSpec`
150+
| `ExampleMatcher`
160151

161152
| String matching
162-
| `ExampleSpec` and property path
153+
| `ExampleMatcher` and property path
163154

164155
| Ignoring properties
165156
| Property path
166157

167158
| Case sensitivity
168-
| `ExampleSpec` and property path
159+
| `ExampleMatcher` and property path
169160

170161
| Value transformation
171162
| Property path
172163

173164
|===
174165

175-
[[query.by.example.examplespec.typed]]
176-
==== Typed Example Spec
177-
You have now seen the usage of untyped `ExampleSpec`. The second flavor of `ExampleSpec` is typed which adds more control over the result type. When executing an `Example` containing a typed `ExampleSpec` the type of the `ExampleSpec` is used as domain type. Control over the domain type is useful in particular when querying along the inheritance hierarchy or the repository contains multiple types within one table/collection/keyspace.
178-
179-
.Sample Person object
180-
====
181-
[source,java]
182-
----
183-
public class SpecialPerson extends Person {
184-
185-
// … more functionality omitted.
186-
}
187-
----
188-
====
189-
190-
.Typed Example Spec with customized matching
191-
====
192-
[source,java]
193-
----
194-
QueryByExampleExecutor<Person> personRepository = … ;
195-
196-
Person person = new Person(); <1>
197-
198-
person.setFirstname("Dave"); <2>
199-
200-
ExampleSpec<SpecialPerson> exampleSpec = ExampleSpec.typed(SpecialPerson.class); <3>
201-
202-
Example<Person> example = Example.of(person, exampleSpec); <4>
203-
204-
List<Person> result = personRepository.findAll(example); <5>
205-
206-
----
207-
<1> Create a new instance of the domain object.
208-
<2> Set properties.
209-
<3> Create a typed `ExampleSpec` for `SpecialPerson` that extends `Person`.
210-
<4> Construct a new `Example` using the typed `ExampleSpec` and the `Person` probe.
211-
<5> Run a query to select all instances of `SpecialPerson` in the repository. Note that the result type is the base class `Person`.
212-
====
213-

src/main/java/org/springframework/data/domain/Example.java

+22-68
Original file line numberDiff line numberDiff line change
@@ -15,84 +15,51 @@
1515
*/
1616
package org.springframework.data.domain;
1717

18-
import org.springframework.util.Assert;
18+
import lombok.AccessLevel;
19+
import lombok.EqualsAndHashCode;
20+
import lombok.NonNull;
21+
import lombok.RequiredArgsConstructor;
22+
import lombok.ToString;
23+
1924
import org.springframework.util.ClassUtils;
2025

2126
/**
2227
* Support for query by example (QBE). An {@link Example} takes a {@code probe} to define the example. Matching options
23-
* and type safety can be tuned using {@link ExampleSpec}. {@link Example} uses {@link ExampleSpec#untyped()}
24-
* {@link ExampleSpec} by default.
25-
* <p>
26-
* This class is immutable.
28+
* and type safety can be tuned using {@link ExampleMatcher}.
2729
*
2830
* @author Christoph Strobl
2931
* @author Mark Paluch
30-
* @param <T>
32+
* @author Oliver Gierke
33+
* @param <T> the type of the probe.
3134
* @since 1.12
3235
*/
36+
@ToString
37+
@EqualsAndHashCode
38+
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
3339
public class Example<T> {
3440

35-
private final T probe;
36-
private final ExampleSpec exampleSpec;
41+
private final @NonNull T probe;
42+
private final @NonNull ExampleMatcher matcher;
3743

3844
/**
3945
* Create a new {@link Example} including all non-null properties by default.
4046
*
41-
* @param probe The probe to use. Must not be {@literal null}.
42-
*/
43-
@SuppressWarnings("unchecked")
44-
private Example(T probe) {
45-
46-
Assert.notNull(probe, "Probe must not be null!");
47-
48-
this.probe = probe;
49-
this.exampleSpec = ExampleSpec.untyped();
50-
}
51-
52-
/**
53-
* Create a new {@link Example} including all non-null properties by default.
54-
*
55-
* @param probe The probe to use. Must not be {@literal null}.
56-
* @param exampleSpec The example specification to use. Must not be {@literal null}.
57-
*/
58-
private Example(T probe, ExampleSpec exampleSpec) {
59-
60-
Assert.notNull(probe, "Probe must not be null!");
61-
62-
this.probe = probe;
63-
this.exampleSpec = exampleSpec;
64-
}
65-
66-
/**
67-
* Create a new {@link Example} including all non-null properties by default using an untyped {@link ExampleSpec}.
68-
*
6947
* @param probe must not be {@literal null}.
7048
* @return
7149
*/
7250
public static <T> Example<T> of(T probe) {
73-
return new Example<T>(probe);
51+
return new Example<T>(probe, ExampleMatcher.matching());
7452
}
7553

7654
/**
77-
* Create a new {@link Example} with a configured untyped {@link ExampleSpec}.
55+
* Create a new {@link Example} using the given {@link ExampleMatcher}.
7856
*
7957
* @param probe must not be {@literal null}.
80-
* @param exampleSpec must not be {@literal null}.
58+
* @param matcher must not be {@literal null}.
8159
* @return
8260
*/
83-
public static <T> Example<T> of(T probe, ExampleSpec exampleSpec) {
84-
return new Example<T>(probe, exampleSpec);
85-
}
86-
87-
/**
88-
* Create a new {@link Example} with a configured {@link TypedExampleSpec}.
89-
*
90-
* @param probe must not be {@literal null}.
91-
* @param exampleSpec must not be {@literal null}.
92-
* @return
93-
*/
94-
public static <T, S extends T> Example<S> of(S probe, TypedExampleSpec<T> exampleSpec) {
95-
return new Example<S>(probe, exampleSpec);
61+
public static <T> Example<T> of(T probe, ExampleMatcher matcher) {
62+
return new Example<T>(probe, matcher);
9663
}
9764

9865
/**
@@ -105,12 +72,12 @@ public T getProbe() {
10572
}
10673

10774
/**
108-
* Get the {@link ExampleSpec} used.
75+
* Get the {@link ExampleMatcher} used.
10976
*
11077
* @return never {@literal null}.
11178
*/
112-
public ExampleSpec getExampleSpec() {
113-
return exampleSpec;
79+
public ExampleMatcher getMatcher() {
80+
return matcher;
11481
}
11582

11683
/**
@@ -124,17 +91,4 @@ public ExampleSpec getExampleSpec() {
12491
public Class<T> getProbeType() {
12592
return (Class<T>) ClassUtils.getUserClass(probe.getClass());
12693
}
127-
128-
/**
129-
* Get the actual result type for the query. The result type can be different when using {@link TypedExampleSpec}.
130-
*
131-
* @return
132-
* @see ClassUtils#getUserClass(Class)
133-
*/
134-
@SuppressWarnings("unchecked")
135-
public <S extends T> Class<S> getResultType() {
136-
return (Class<S>) (exampleSpec instanceof TypedExampleSpec<?> ? ((TypedExampleSpec<T>) exampleSpec).getType()
137-
: getProbeType());
138-
}
139-
14094
}

0 commit comments

Comments
 (0)