Querying by example

So far, we've built up several reactive queries using property navigation. And we've updated ImageService to reactively transform our queried results into operations needed to support our social media platform.

But something that may not be apparent in the design of our data API is the fact that our method signatures are tied to the properties directly. This means that if a domain field changes, we would have to update the queries, or they will break.

There are other issues we might run into, such as offering the ability to put a filter on our web page, and letting the user fetch a subset of images based on their needs.

What if we had a system that listed information about employees. If we imagined writing a finder that lets a user enter firstName, lastName, and age range, it would probably look like this:

    interface PersonRepository 
     extends ReactiveCrudRepository<Person, Long> { 
 
       List<Person> findByFirstNameAndLastNameAndAgeBetween( 
         String firstName, String lastName, int from, int to); 
    } 

Yikes! That's ugly. (Even worse, imagine making all the strings case insensitive!)

All of these things lead us toward an alternative Spring Data solution--Query by Example.

Query by Example, simply stated, has us assemble a domain object with the criteria provided, and submit them to a query. Let's look at an example. Assume we were storing Employee records like this:

    @Data 
    @Document 
    public class Employee { 
 
      @Id private String id; 
      private String firstName; 
      private String lastName; 
      private String role; 
    } 

This preceding example is a very simple domain object, and can be explained as follows:

  • Lombok's @Data annotation provides getters, setters, equals, hashCode, and toString methods
  • Spring Data MongoDB's @Document annotation indicates this POJO is a target for storage in MongoDB
  • Spring Data Commons' @Id annotation indicates that the id field is the identifier
  • The rest of the fields are simple strings

Next, we need to define a repository as we did earlier, but we must also mix in another interface that gives us a standard complement of Query by Example operations. We can do that with the following definition:

    public interface EmployeeRepository extends 
     ReactiveCrudRepository<Employee, String>, 
     ReactiveQueryByExampleExecutor<Employee> { 
 
    } 

This last repository definition can be explained as follows:

  • It's an interface declaration, meaning, we don't write any implementation code
  • ReactiveCrudRepository provides the standard CRUD operations with reactive options (Mono and Flux return types, and more)
  • ReactiveQueryByExampleExecutor is a mix-in interface that introduces the Query by Example operations which we'll poke at shortly

Once again, with just a domain object and a Spring Data repository defined, we have all the tools to go forth and query MongoDB!

First things first, we should again use blocking MongoOperations to preload some data like this:

    mongoOperations.dropCollection(Employee.class); 
 
    Employee e1 = new Employee(); 
    e1.setId(UUID.randomUUID().toString()); 
    e1.setFirstName("Bilbo"); 
    e1.setLastName("Baggins"); 
    e1.setRole("burglar"); 
 
    mongoOperations.insert(e1); 
 
    Employee e2 = new Employee(); 
    e2.setId(UUID.randomUUID().toString()); 
    e2.setFirstName("Frodo"); 
    e2.setLastName("Baggins"); 
    e2.setRole("ring bearer"); 
 
    mongoOperations.insert(e2); 

The preceding setup can be described as follows:

  • Start by using dropCollection to clean things out
  • Next, create a new Employee, and insert it into MongoDB
  • Create a second Employee and insert it as well
Only use MongoOperations to preload test data. Do NOT use it for production code, or your efforts at building reactive apps will be for nothing.

With our data preloaded, let's take a closer look at that ReactiveQueryByExampleExecutor interface used to define our repository (provided by Spring Data Commons). Digging in, we can find a couple of key query signatures like this:

    <S extends T> Mono<S> findOne(Example<S> example); 
    <S extends T> Flux<S> findAll(Example<S> example); 

Neither of these aforementioned methods have any properties whatsoever in their names compared to finders like findByLastName. The big difference is the usage of Example as an argument. Example is a container provided by Spring Data Commons to define the parameters of a query.

What does such an Example object look like? Let's construct one right now!

    Employee e = new Employee(); 
    e.setFirstName("Bilbo"); 
    Example<Employee> example = Example.of(e); 

This construction of an Example is described as follows:

  • We create an Employee probe named e
  • We set the probe's firstName to Bilbo
  • Then we leverage the Example.of static helper to turn the probe into an Example
In this example, the probe is hard coded, but in production, the value would be pulled from the request whether it was part of a REST route, the body of a web request, or somewhere else.

Before we actually use the Example to conduct a query, it pays to understand what an Example object is. Simply put, an Example consists of a probe and a matcher. The probe is the POJO object containing all the values we wish to use as criteria. The matcher is an ExampleMatcher that governs how the probe is used. We'll see different types of matching in the following various usages.

Proceeding with our Example in hand, we can now solicit a response from the repository as follows:

    Mono<Employee> singleEmployee = repository.findOne(example); 

We no longer have to put firstName in the query's method signature. Instead, it has become a parameter fed to the query through the Example input.

Examples, by default, only query against non-null fields. That's a fancy way of saying that only the fields populated in the probe are considered. Also, the values supplied must match the stored records exactly. This is the default matcher used in the Example objects.

Since an exact match isn't always what's needed, let's see how we can adjust things, and come up with a different match criteria, as shown in this code:

    Employee e = new Employee(); 
    e.setLastName("baggins"); // Lowercase lastName 
 
    ExampleMatcher matcher = ExampleMatcher.matching() 
     .withIgnoreCase() 
     .withMatcher("lastName", startsWith()) 
     .withIncludeNullValues(); 
 
    Example<Employee> example = Example.of(e, matcher); 

This preceding example can be described as follows:

  • We create another Employee probe
  • We deliberately set the lastName value as lowercase
  • Then we create a custom ExampleMatcher using matching()
  • withIgnoreCase says to ignore the case of the values being checked
  • withMatcher lets us indicate that a given document's lastName starts with the probe's value
  • withIncludeNullValues will also match any entries that have nulled-out values
  • Finally, we create an Example using our probe, but with this custom matcher

With this highly customized example, we can query for ALL employees matching these criteria:

    Flux<Employee> multipleEmployees = repository.findAll(example); 

This last code simply uses the findAll query, that returns a Flux using the same example criteria.

Remember how we briefly mentioned that Query by Example can lend itself to a form on a web page where various fields are filled out? Based on the fields, the user can decide what to fetch. Notice how we used withIgnoreCase? By default, that flag flips to true, but it's possible to feed it a Boolean. It means we can put a checkbox on the web page allowing the user to decide whether or not to ignore case in their search.

Simple or complex, Query by Example provides flexible options to query for results. And using Reactor types, we can get just about anything we need with the two queries provided: findOne or findAll.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.90.182