Files
spring-data-commons/src/main/antora/modules/ROOT/pages/query-by-example.adoc
Jens Schauder ccf3e04453 Refining QBE documentation.
Adds a configurable limitation about collection support in Query By Example.

Closes: #3226
2025-01-10 10:21:14 +01:00

222 lines
8.3 KiB
Plaintext

ifndef::support-qbe-collection[]
:support-qbe-collection: true
endif::[]
[[query-by-example]]
= Query by Example
[[query-by-example.introduction]]
== Introduction
This chapter provides an introduction to Query by Example and explains how to use it.
Query by Example (QBE) is a user-friendly querying technique with a simple interface.
It allows dynamic query creation and does not require you to write queries that contain field names.
In fact, Query by Example does not require you to write queries by using store-specific query languages at all.
NOTE: This chapter explains the core concepts of Query by Example.
The information is pulled from the Spring Data Commons module.
Depending on your database, String matching support can be limited.
[[query-by-example.usage]]
== Usage
The Query by Example API consists of four parts:
* Probe: The actual example of a domain object with populated fields.
* `ExampleMatcher`: The `ExampleMatcher` carries details on how to match particular fields.
It can be reused across multiple Examples.
* `Example`: An `Example` consists of the probe and the `ExampleMatcher`.
It is used to create the query.
* `FetchableFluentQuery`: A `FetchableFluentQuery` offers a fluent API, that allows further customization of a query derived from an `Example`.
Using the fluent API lets you specify ordering projection and result processing for your query.
Query by Example is well suited for several use cases:
* Querying your data store with a set of static or dynamic constraints.
* Frequent refactoring of the domain objects without worrying about breaking existing queries.
* Working independently of the underlying data store API.
Query by Example also has several limitations:
* No support for nested or grouped property constraints, such as `firstname = ?0 or (firstname = ?1 and lastname = ?2)`.
ifeval::[{support-qbe-collection} != true]
* No support for matching collections or maps.
endif::[]
* Store-specific support on string matching.
Depending on your databases, String matching can support starts/contains/ends/regex for strings.
* Exact matching for other property types.
Before getting started with Query by Example, you need to have a domain object.
To get started, create an interface for your repository, as shown in the following example:
.Sample Person object
[source,java]
----
public class Person {
@Id
private String id;
private String firstname;
private String lastname;
private Address address;
// … getters and setters omitted
}
----
The preceding example shows a simple domain object.
You can use it to create an `Example`.
By default, fields having `null` values are ignored, and strings are matched by using the store specific defaults.
NOTE: Inclusion of properties into a Query by Example criteria is based on nullability.
Properties using primitive types (`int`, `double`, …) are always included unless the <<query-by-example.matchers,`ExampleMatcher` ignores the property path>>.
Examples can be built by either using the `of` factory method or by using <<query-by-example.matchers,`ExampleMatcher`>>. `Example` is immutable.
The following listing shows a simple Example:
.Simple Example
====
[source,java]
----
Person person = new Person(); <1>
person.setFirstname("Dave"); <2>
Example<Person> example = Example.of(person); <3>
----
<1> Create a new instance of the domain object.
<2> Set the properties to query.
<3> Create the `Example`.
====
You can run the example queries by using repositories.
To do so, let your repository interface extend `QueryByExampleExecutor<T>`.
The following listing shows an excerpt from the `QueryByExampleExecutor` interface:
.The `QueryByExampleExecutor`
[source,java]
----
public interface QueryByExampleExecutor<T> {
<S extends T> S findOne(Example<S> example);
<S extends T> Iterable<S> findAll(Example<S> example);
// … more functionality omitted.
}
----
[[query-by-example.matchers]]
== Example Matchers
Examples are not limited to default settings.
You can specify your own defaults for string matching, null handling, and property-specific settings by using the `ExampleMatcher`, as shown in the following example:
.Example matcher with customized matching
====
[source,java]
----
Person person = new Person(); <1>
person.setFirstname("Dave"); <2>
ExampleMatcher matcher = ExampleMatcher.matching() <3>
.withIgnorePaths("lastname") <4>
.withIncludeNullValues() <5>
.withStringMatcher(StringMatcher.ENDING); <6>
Example<Person> example = Example.of(person, matcher); <7>
----
<1> Create a new instance of the domain object.
<2> Set properties.
<3> Create an `ExampleMatcher` to expect all values to match.
It is usable at this stage even without further configuration.
<4> Construct a new `ExampleMatcher` to ignore the `lastname` property path.
<5> Construct a new `ExampleMatcher` to ignore the `lastname` property path and to include null values.
<6> Construct a new `ExampleMatcher` to ignore the `lastname` property path, to include null values, and to perform suffix string matching.
<7> Create a new `Example` based on the domain object and the configured `ExampleMatcher`.
====
By default, the `ExampleMatcher` expects all values set on the probe to match.
If you want to get results matching any of the predicates defined implicitly, use `ExampleMatcher.matchingAny()`.
You can specify behavior for individual properties (such as "firstname" and "lastname" or, for nested properties, "address.city").
You can tune it with matching options and case sensitivity, as shown in the following example:
.Configuring matcher options
[source,java]
----
ExampleMatcher matcher = ExampleMatcher.matching()
.withMatcher("firstname", endsWith())
.withMatcher("lastname", startsWith().ignoreCase());
}
----
Another way to configure matcher options is to use lambdas (introduced in Java 8).
This approach creates a callback that asks the implementor to modify the matcher.
You need not return the matcher, because configuration options are held within the matcher instance.
The following example shows a matcher that uses lambdas:
.Configuring matcher options with lambdas
[source,java]
----
ExampleMatcher matcher = ExampleMatcher.matching()
.withMatcher("firstname", match -> match.endsWith())
.withMatcher("firstname", match -> match.startsWith());
}
----
Queries created by `Example` use a merged view of the configuration.
Default matching settings can be set at the `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.
The following table describes the scope of the various `ExampleMatcher` settings:
[cols="1,2",options="header"]
.Scope of `ExampleMatcher` settings
|===
| Setting
| Scope
| Null-handling
| `ExampleMatcher`
| String matching
| `ExampleMatcher` and property path
| Ignoring properties
| Property path
| Case sensitivity
| `ExampleMatcher` and property path
| Value transformation
| Property path
|===
[[query-by-example.fluent]]
== Fluent API
`QueryByExampleExecutor` offers one more method, which we did not mention so far: `<S extends T, R> R findBy(Example<S> example, Function<FluentQuery.FetchableFluentQuery<S>, R> queryFunction)`.
As with other methods, it executes a query derived from an `Example`.
However, with the second argument, you can control aspects of that execution that you cannot dynamically control otherwise.
You do so by invoking the various methods of the `FetchableFluentQuery` in the second argument.
`sortBy` lets you specify an ordering for your result.
`as` lets you specify the type to which you want the result to be transformed.
`project` limits the queried attributes.
`first`, `firstValue`, `one`, `oneValue`, `all`, `page`, `stream`, `count`, and `exists` define what kind of result you get and how the query behaves when more than the expected number of results are available.
.Use the fluent API to get the last of potentially many results, ordered by lastname.
[source,java]
----
Optional<Person> match = repository.findBy(example,
q -> q
.sortBy(Sort.by("lastname").descending())
.first()
);
----