Skip to content

Commit b0ce700

Browse files
mp911deodrotbohm
authored andcommitted
DATACMNS-810 - Refactor Query by Example API.
Split Example and ExampleSpec to create reusable components. Refactor builder pattern to a fluent API that creates immutable instances. Split user and framework API, Example and ExampleSpec are user API, created ExampleSpecAccessor for modules to access example spec configuration. Create static methods in GenericPropertyMatchers to ease creation of matchers in a readable style. Convert PropertySpecifier to inner class and move PropertySpecifiers to ExampleSpec. Related tickets: DATAJPA-218, DATAMONGO-1245. Original pull request: #153.
1 parent 4092f07 commit b0ce700

File tree

12 files changed

+2769
-884
lines changed

12 files changed

+2769
-884
lines changed
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
[[query.by.example]]
2+
== Query by Example
3+
4+
=== Introduction
5+
6+
This chapter will give you an introduction to Query by Example and explain how to use Examples.
7+
8+
Query by Example (QBE) is a user-friendly querying technique with a simple interface. It allows dynamic query creation and does not require to write queries containing field names. In fact, Query by Example does not require to write queries using store-specific query languages at all.
9+
10+
=== Usage
11+
12+
The Query by Example API consists of three parts:
13+
14+
* 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`.
17+
18+
Query by Example is suited for several use-cases but also comes with limitations:
19+
20+
**When to use**
21+
22+
* Querying your data store with a set of static or dynamic constraints
23+
* Frequent refactoring of the domain objects without worrying about breaking existing queries
24+
* Works independently from the underlying data store API
25+
26+
**Limitations**
27+
28+
* Query predicates are combined using the `AND` keyword
29+
* No support for nested/grouped property constraints like `firstname = ?0 or (firstname = ?1 and lastname = ?2)`
30+
* Only supports starts/contains/ends/regex matching for strings and exact matching for other property types
31+
32+
Before getting started with Query by Example, you need to have a domain object. To get started, simply create an interface for your repository:
33+
34+
.Sample Person object
35+
====
36+
[source,java]
37+
----
38+
public class Person {
39+
40+
@Id
41+
private String id;
42+
private String firstname;
43+
private String lastname;
44+
private Address address;
45+
46+
// … getters and setters omitted
47+
}
48+
----
49+
====
50+
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.
52+
53+
.Simple Example
54+
====
55+
[source,java]
56+
----
57+
Person person = new Person(); <1>
58+
59+
person.setFirstname("Dave"); <2>
60+
61+
Example<Person> example = Example.of(person); <3>
62+
----
63+
<1> Create a new instance of the domain object
64+
<2> Set the properties to query
65+
<3> Create the `Example`
66+
====
67+
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:
71+
72+
.The `QueryByExampleExecutor`
73+
====
74+
[source, java]
75+
----
76+
public interface QueryByExampleExecutor<T> {
77+
78+
<S extends T> S findOne(Example<S> example);
79+
80+
<S extends T> Iterable<S> findAll(Example<S> example);
81+
82+
// … more functionality omitted.
83+
}
84+
----
85+
====
86+
87+
You can read more about <<query.by.example.execution, Query by Example Execution>> below.
88+
89+
[[query.by.example.examplespec]]
90+
=== Example Spec
91+
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.
93+
94+
.Untyped Example Spec with customized matching
95+
====
96+
[source,java]
97+
----
98+
Person person = new Person(); <1>
99+
100+
person.setFirstname("Dave"); <2>
101+
102+
ExampleSpec exampleSpec = ExampleSpec.untyped() <3>
103+
104+
.withIgnorePaths("lastname") <4>
105+
106+
.withIncludeNullValues() <5>
107+
108+
.withStringMatcherEnding(); <6>
109+
110+
Example<Person> example = Example.of(person, exampleSpec); <7>
111+
112+
----
113+
<1> Create a new instance of the domain object.
114+
<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`.
120+
====
121+
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+
124+
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.
125+
126+
.Configuring matcher options
127+
====
128+
[source,java]
129+
----
130+
ExampleSpec exampleSpec = ExampleSpec.untyped()
131+
.withMatcher("firstname", endsWith())
132+
.withMatcher("lastname", startsWith().ignoreCase());
133+
}
134+
----
135+
====
136+
137+
Another style to configure matcher options is by using Java 8 lambdas. This approach is a callback that asks the implementor to modify the matcher. It's not required to return the matcher because configuration options are held within the matcher instance.
138+
139+
.Configuring matcher options with lambdas
140+
====
141+
[source,java]
142+
----
143+
ExampleSpec exampleSpec = ExampleSpec.untyped()
144+
.withMatcher("firstname", matcher -> matcher.endsWith())
145+
.withMatcher("firstname", matcher -> matcher.startsWith());
146+
}
147+
----
148+
====
149+
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.
151+
152+
[cols="1,2", options="header"]
153+
.Scope of `ExampleSpec` settings
154+
|===
155+
| Setting
156+
| Scope
157+
158+
| Null-handling
159+
| `ExampleSpec`
160+
161+
| String matching
162+
| `ExampleSpec` and property path
163+
164+
| Ignoring properties
165+
| Property path
166+
167+
| Case sensitivity
168+
| `ExampleSpec` and property path
169+
170+
| Value transformation
171+
| Property path
172+
173+
|===
174+
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+

0 commit comments

Comments
 (0)