1. Introduction
In this tutorial, we’ll learn how to query data with the Spring Data Query by Example API.
First, we’ll define the schema of the data we want to query. Next, we’ll examine a few of the relevant classes from Spring Data. Finally, we’ll run through a few examples.
Let’s get started!
2. The Test Data
Our test data is a list of passenger names, as well as the seat they occupied:
First Name
Last Name
Seat Number
Jill
Smith
50
Eve
Jackson
94
Fred
Bloggs
22
Ricki
Bobbie
36
Siya
Kolisi
85
3. Domain
Let’s create the Spring Data Repository we’ll need, and provide our domain class and id type.
To begin, we’ll model our Passenger as a JPA entity:
@Entity
class Passenger {
@Id
@GeneratedValue
@Column(nullable = false)
private Long id;
@Basic(optional = false)
@Column(nullable = false)
private String firstName;
@Basic(optional = false)
@Column(nullable = false)
private String lastName;
@Basic(optional = false)
@Column(nullable = false)
private int seatNumber;
// constructor, getters etc.
}
Instead of using JPA, we could’ve modeled it as another abstraction.
4. Query by Example API
First, let’s take a look at the JpaRepository interface. As we can see, it extends the QueryByExampleExecutor interface to support query by example:
public interface JpaRepository<T, ID>
extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {}
This interface introduces more variants of the find() method that we’re familiar with from Spring Data. However, each method also accepts an instance of Example:
public interface QueryByExampleExecutor<T> {
<S extends T> Optional<S> findOne(Example<S> var1);
<S extends T> Iterable<S> findAll(Example<S> var1);
<S extends T> Iterable<S> findAll(Example<S> var1, Sort var2);
<S extends T> Page<S> findAll(Example<S> var1, Pageable var2);
<S extends T> long count(Example<S> var1);
<S extends T> boolean exists(Example<S> var1);
}
Next, the Example interface exposes methods to access the probe and the ExampleMatcher.
It’s important to realize that the probe is the instance of our Entity:
public interface Example<T> {
static <T> org.springframework.data.domain.Example<T> of(T probe) {
return new TypedExample(probe, ExampleMatcher.matching());
}
static <T> org.springframework.data.domain.Example<T> of(T probe, ExampleMatcher matcher) {
return new TypedExample(probe, matcher);
}
T getProbe();
ExampleMatcher getMatcher();
default Class<T> getProbeType() {
return ProxyUtils.getUserClass(this.getProbe().getClass());
}
}
In short, our probe and ExampleMatcher together specify our query.
5. Limitations
Like all things, the Query by Example API has some limitations:
- Nesting and grouping statements aren’t supported. For example: *(*firstName = ?0 and lastName = ?1) or seatNumber = ?2
- String matching only includes exact, case-insensitive, starts, ends, contains, and regex
- All types other than String are exact-match only
Now that we’re a little more familiar with the API and its limitations, let’s dive into some examples.
6. Examples
6.1. Case-Sensitive Matching
Let’s start with a simple example, and talk about the default behavior:
@Test
public void givenPassengers_whenFindByExample_thenExpectedReturned() {
Example<Passenger> example = Example.of(Passenger.from("Fred", "Bloggs", null));
Optional<Passenger> actual = repository.findOne(example);
assertTrue(actual.isPresent());
assertEquals(Passenger.from("Fred", "Bloggs", 22), actual.get());
}
In particular, the static Example.of() method builds an Example using ExampleMatcher.matching().
In other words, an exact match will be performed on all non-null properties of Passenger. Thus, the matching is case-sensitive on String properties.
However, it wouldn’t be too useful if all we could do was an exact match on all non-null properties.
This is where the ExampleMatcher comes in. By building our own ExampleMatcher, we can customize the behavior to suit our needs.
6.2. Case-Insensitive Matching
With that in mind, let’s have a look at another example, this time using withIgnoreCase() to achieve case-insensitive matching:
@Test
public void givenPassengers_whenFindByExampleCaseInsensitiveMatcher_thenExpectedReturned() {
ExampleMatcher caseInsensitiveExampleMatcher = ExampleMatcher.matchingAll().withIgnoreCase();
Example<Passenger> example = Example.of(Passenger.from("fred", "bloggs", null),
caseInsensitiveExampleMatcher);
Optional<Passenger> actual = repository.findOne(example);
assertTrue(actual.isPresent());
assertEquals(Passenger.from("Fred", "Bloggs", 22), actual.get());
}
In this example, notice that we first called ExampleMatcher.matchingAll(); it has the same behavior as ExampleMatcher.matching(), which we used in the previous example.
6.3. Custom Matching
We can also tune the behavior of our matcher on a per-property basis and match any property using ExampleMatcher.matchingAny():
@Test
public void givenPassengers_whenFindByExampleCustomMatcher_thenExpectedReturned() {
Passenger jill = Passenger.from("Jill", "Smith", 50);
Passenger eve = Passenger.from("Eve", "Jackson", 95);
Passenger fred = Passenger.from("Fred", "Bloggs", 22);
Passenger siya = Passenger.from("Siya", "Kolisi", 85);
Passenger ricki = Passenger.from("Ricki", "Bobbie", 36);
ExampleMatcher customExampleMatcher = ExampleMatcher.matchingAny()
.withMatcher("firstName", ExampleMatcher.GenericPropertyMatchers.contains().ignoreCase())
.withMatcher("lastName", ExampleMatcher.GenericPropertyMatchers.contains().ignoreCase());
Example<Passenger> example = Example.of(Passenger.from("e", "s", null), customExampleMatcher);
List<Passenger> passengers = repository.findAll(example);
assertThat(passengers, contains(jill, eve, fred, siya));
assertThat(passengers, not(contains(ricki)));
}
6.4. Ignoring Properties
We may also only want to query on a subset of our properties.
We achieve this by ignoring some properties using ExampleMatcher.ignorePaths(String… paths):
@Test
public void givenPassengers_whenFindByIgnoringMatcher_thenExpectedReturned() {
Passenger jill = Passenger.from("Jill", "Smith", 50);
Passenger eve = Passenger.from("Eve", "Jackson", 95);
Passenger fred = Passenger.from("Fred", "Bloggs", 22);
Passenger siya = Passenger.from("Siya", "Kolisi", 85);
Passenger ricki = Passenger.from("Ricki", "Bobbie", 36);
ExampleMatcher ignoringExampleMatcher = ExampleMatcher.matchingAny()
.withMatcher("lastName", ExampleMatcher.GenericPropertyMatchers.startsWith().ignoreCase())
.withIgnorePaths("firstName", "seatNumber");
Example<Passenger> example = Example.of(Passenger.from(null, "b", null), ignoringExampleMatcher);
List<Passenger> passengers = repository.findAll(example);
assertThat(passengers, contains(fred, ricki));
assertThat(passengers, not(contains(jill));
assertThat(passengers, not(contains(eve));
assertThat(passengers, not(contains(siya));
}
7. Conclusion
In this article, we demonstrated how to use the Query by Example API.
We learned how to use Example and ExampleMatcher, along with the QueryByExampleExecutor interface to query a table using an example data instance.
As always, all the source code is available over on GitHub.