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.
