Skip to content

Commit f6007ec

Browse files
committed
DATACMNS-810 - Add common documentation for Query by Example.
1 parent c9d9f90 commit f6007ec

File tree

1 file changed

+173
-0
lines changed

1 file changed

+173
-0
lines changed
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
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.
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`>>. Once the `Example` is constructed it 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> T findOne(Example<S> example);
79+
80+
<S extends T> Iterable<T> 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`.
93+
94+
.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<Person> exampleSpec = ExampleSpec.of(Person.class) <3>
103+
104+
.withIgnorePaths("lastname") <4>
105+
106+
.withIncludeNullValues() <5>
107+
108+
.withStringMatcherEnding(); <6>
109+
110+
Example 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 `ExampleSpec` for the `Person` type. 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 once it is created. Calls to `with…(…)` create each time a new `ExampleSpec` that carry all settings from the originating instance and setting the specific configuration of the `with…(…)` so 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<Person> example = ExampleSpec.of(Person.class)
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<Person> example = ExampleSpec.of(Person.class)
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+
|===

0 commit comments

Comments
 (0)