Skip to content

DATACMNS-810 - Add core types for Query By Example support. #153

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
<version>1.12.0.BUILD-SNAPSHOT</version>
<version>1.12.0.DATACMNS-810-SNAPSHOT</version>

<name>Spring Data Core</name>

Expand Down
213 changes: 213 additions & 0 deletions src/main/asciidoc/query-by-example.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
[[query.by.example]]
== Query by Example

=== Introduction

This chapter will give you an introduction to Query by Example and explain how to use Examples.

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.

=== Usage

The Query by Example API consists of three parts:

* Probe: That is the actual example of a domain object with populated fields.
* `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>>.
* `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`.

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

**When to use**

* Querying your data store with a set of static or dynamic constraints
* Frequent refactoring of the domain objects without worrying about breaking existing queries
* Works independently from the underlying data store API

**Limitations**

* Query predicates are combined using the `AND` keyword
* No support for nested/grouped property constraints like `firstname = ?0 or (firstname = ?1 and lastname = ?2)`
* Only supports starts/contains/ends/regex matching for strings and exact matching for other property types

Before getting started with Query by Example, you need to have a domain object. To get started, simply create an interface for your repository:

.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
}
----
====

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.

.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`
====

NOTE: Property names of the sample object must correlate with the property names of the queried domain object.

Examples can be executed ideally with Repositories. To do so, let your repository extend from `QueryByExampleExecutor`, here's 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.
}
----
====

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

[[query.by.example.examplespec]]
=== Example Spec

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.

.Untyped Example Spec with customized matching
====
[source,java]
----
Person person = new Person(); <1>

person.setFirstname("Dave"); <2>

ExampleSpec exampleSpec = ExampleSpec.untyped() <3>

.withIgnorePaths("lastname") <4>

.withIncludeNullValues() <5>

.withStringMatcherEnding(); <6>

Example<Person> example = Example.of(person, exampleSpec); <7>

----
<1> Create a new instance of the domain object.
<2> Set properties.
<3> Create an untyped `ExampleSpec`. The `ExampleSpec` is usable at this stage.
<4> Construct a new `ExampleSpec` to ignore the property path `lastname`.
<5> Construct a new `ExampleSpec` to ignore the property path `lastname` and to include null values.
<6> Construct a new `ExampleSpec` to ignore the property path `lastname`, to include null values, and use perform suffix string matching.
<7> Create a new `Example` based on the domain object and the configured `ExampleSpec`.
====

`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.

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.

.Configuring matcher options
====
[source,java]
----
ExampleSpec exampleSpec = ExampleSpec.untyped()
.withMatcher("firstname", endsWith())
.withMatcher("lastname", startsWith().ignoreCase());
}
----
====

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.

.Configuring matcher options with lambdas
====
[source,java]
----
ExampleSpec exampleSpec = ExampleSpec.untyped()
.withMatcher("firstname", matcher -> matcher.endsWith())
.withMatcher("firstname", matcher -> matcher.startsWith());
}
----
====

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.

[cols="1,2", options="header"]
.Scope of `ExampleSpec` settings
|===
| Setting
| Scope

| Null-handling
| `ExampleSpec`

| String matching
| `ExampleSpec` and property path

| Ignoring properties
| Property path

| Case sensitivity
| `ExampleSpec` and property path

| Value transformation
| Property path

|===

[[query.by.example.examplespec.typed]]
==== Typed Example Spec
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.

.Sample Person object
====
[source,java]
----
public class SpecialPerson extends Person {

// … more functionality omitted.
}
----
====

.Typed Example Spec with customized matching
====
[source,java]
----
QueryByExampleExecutor<Person> personRepository = … ;

Person person = new Person(); <1>

person.setFirstname("Dave"); <2>

ExampleSpec<SpecialPerson> exampleSpec = ExampleSpec.typed(SpecialPerson.class); <3>

Example<Person> example = Example.of(person, exampleSpec); <4>

List<Person> result = personRepository.findAll(example); <5>

----
<1> Create a new instance of the domain object.
<2> Set properties.
<3> Create a typed `ExampleSpec` for `SpecialPerson` that extends `Person`.
<4> Construct a new `Example` using the typed `ExampleSpec` and the `Person` probe.
<5> Run a query to select all instances of `SpecialPerson` in the repository. Note that the result type is the base class `Person`.
====

140 changes: 140 additions & 0 deletions src/main/java/org/springframework/data/domain/Example.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.domain;

import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;

/**
* Support for query by example (QBE). An {@link Example} takes a {@code probe} to define the example. Matching options
* and type safety can be tuned using {@link ExampleSpec}. {@link Example} uses {@link ExampleSpec#untyped()}
* {@link ExampleSpec} by default.
* <p>
* This class is immutable.
*
* @author Christoph Strobl
* @author Mark Paluch
* @param <T>
* @since 1.12
*/
public class Example<T> {

private final T probe;
private final ExampleSpec exampleSpec;

/**
* Create a new {@link Example} including all non-null properties by default.
*
* @param probe The probe to use. Must not be {@literal null}.
*/
@SuppressWarnings("unchecked")
private Example(T probe) {

Assert.notNull(probe, "Probe must not be null!");

this.probe = probe;
this.exampleSpec = ExampleSpec.untyped();
}

/**
* Create a new {@link Example} including all non-null properties by default.
*
* @param probe The probe to use. Must not be {@literal null}.
* @param exampleSpec The example specification to use. Must not be {@literal null}.
*/
private Example(T probe, ExampleSpec exampleSpec) {

Assert.notNull(probe, "Probe must not be null!");

this.probe = probe;
this.exampleSpec = exampleSpec;
}

/**
* Create a new {@link Example} including all non-null properties by default using an untyped {@link ExampleSpec}.
*
* @param probe must not be {@literal null}.
* @return
*/
public static <T> Example<T> of(T probe) {
return new Example<T>(probe);
}

/**
* Create a new {@link Example} with a configured untyped {@link ExampleSpec}.
*
* @param probe must not be {@literal null}.
* @param exampleSpec must not be {@literal null}.
* @return
*/
public static <T> Example<T> of(T probe, ExampleSpec exampleSpec) {
return new Example<T>(probe, exampleSpec);
}

/**
* Create a new {@link Example} with a configured {@link TypedExampleSpec}.
*
* @param probe must not be {@literal null}.
* @param exampleSpec must not be {@literal null}.
* @return
*/
public static <T, S extends T> Example<S> of(S probe, TypedExampleSpec<T> exampleSpec) {
return new Example<S>(probe, exampleSpec);
}

/**
* Get the example used.
*
* @return never {@literal null}.
*/
public T getProbe() {
return probe;
}

/**
* Get the {@link ExampleSpec} used.
*
* @return never {@literal null}.
*/
public ExampleSpec getExampleSpec() {
return exampleSpec;
}

/**
* Get the actual type for the probe used. This is usually the given class, but the original class in case of a
* CGLIB-generated subclass.
*
* @return
* @see ClassUtils#getUserClass(Class)
*/
@SuppressWarnings("unchecked")
public Class<T> getProbeType() {
return (Class<T>) ClassUtils.getUserClass(probe.getClass());
}

/**
* Get the actual result type for the query. The result type can be different when using {@link TypedExampleSpec}.
*
* @return
* @see ClassUtils#getUserClass(Class)
*/
@SuppressWarnings("unchecked")
public <S extends T> Class<S> getResultType() {
return (Class<S>) (exampleSpec instanceof TypedExampleSpec<?> ? ((TypedExampleSpec<T>) exampleSpec).getType()
: getProbeType());
}

}
Loading