Close

Spring Data JPA - Using ExampleMatcher to customize Query by Example

[Last Updated: Oct 26, 2018]

As we saw in the last tutorial that Query By Example (QBE) can be carried out by using an Example instance which takes a Probe (an entity instance populated with the desired fields' values). Example can also use ExampleMatcher which carries the details on how to match particular fields.

Example interface:

package org.springframework.data.domain;
 ....
public interface Example<T> {
        //Creates a new Example with all non-null properties by default.
	static <T> Example<T> of(T probe) {
		return new TypedExample<>(probe, ExampleMatcher.matching());
	}

        //Creates a new Example using the given ExampleMatcher.
	static <T> Example<T> of(T probe, ExampleMatcher matcher) {
		return new TypedExample<>(probe, matcher);
	}
        ....
}

In the last tutorial we saw the use of the first method Example.of(probe) which, by default, uses an instance of ExampleMatcher created by ExampleMatcher.matching(). That targets all non-null properties to generate the query.

In this tutorial we will see how to use the second method Example.of(probe, matcher) and how to create an instance of ExampleMatcher to customize the underlying query.

ExampleMatcher interface:

package org.springframework.data.domain;
 ....
public interface ExampleMatcher {
    //targets all non-null properties ('and' conjunction among different properties of the same entity)
    static ExampleMatcher matching() {return matchingAll();}
    //targets any non-null properties ('or' conjunction among different properties of the same entity)
    static ExampleMatcher matchingAny() {....}
    //targets all non-null properties (same as matching())
    static ExampleMatcher matchingAll() {....}
    //ignores provided property paths
    ExampleMatcher withIgnorePaths(String... ignoredPaths);
    //matches string as per the provided stringMatcher (an enum)
    ExampleMatcher withStringMatcher(StringMatcher stringMatcher);
    //ignores case 
    default ExampleMatcher withIgnoreCase() {....}
    //turn on/off case sensitivity via provided defaultIgnoreCase
    ExampleMatcher withIgnoreCase(boolean defaultIgnoreCase);
    //creates matcher with the specified matcherConfigurer and propertyPath.
    default ExampleMatcher withMatcher(String propertyPath, 
                                MatcherConfigurer<GenericPropertyMatcher> matcherConfigurer) {....}
    //creates matcher with the specified genericPropertyMatcher and propertyPath.
    ExampleMatcher withMatcher(String propertyPath, GenericPropertyMatcher genericPropertyMatcher);
    //creates matcher with the specified  propertyValueTransformer
    ExampleMatcher withTransformer(String propertyPath, 
                                PropertyValueTransformer propertyValueTransformer);
    //creates a case insensitive matcher for the specified propertyPaths
    ExampleMatcher withIgnoreCase(String... propertyPaths);
    //includes null values 
    default ExampleMatcher withIncludeNullValues() {....}
    //ignores null values 
    default ExampleMatcher withIgnoreNullValues() {....}
    //creates matcher with the specified nullHander
    ExampleMatcher withNullHandler(NullHandler nullHandler);
    
     //Null handling
      enum NullHandler {INCLUDE, IGNORE}

     //configures a matcher
     interface MatcherConfigurer<T> {
		void configureMatcher(T matcher);
     }
	....
      //A matcher that specifies StringMatcher string matching and case sensitivity. 
      class GenericPropertyMatcher {
	   public static GenericPropertyMatcher of(StringMatcher stringMatcher, boolean ignoreCase){..}
	   public static GenericPropertyMatcher of(StringMatcher stringMatcher) {....}
	   public GenericPropertyMatcher ignoreCase() {....}
	   public GenericPropertyMatcher ignoreCase(boolean ignoreCase) {....}
	   public GenericPropertyMatcher caseSensitive() {}
	   public GenericPropertyMatcher contains() {....}
	   public GenericPropertyMatcher endsWith() {....}
	   public GenericPropertyMatcher startsWith() {....}
	   public GenericPropertyMatcher exact() {....}
	   public GenericPropertyMatcher storeDefaultMatching() {....}
	   public GenericPropertyMatcher regex() {....}
	   public GenericPropertyMatcher stringMatcher(StringMatcher stringMatcher) {....}
	   public GenericPropertyMatcher transform(PropertyValueTransformer propertyValueTransformer){..}
	}
        ....
      //Predefined GenericPropertyMatcher
      class GenericPropertyMatchers {
	   public static GenericPropertyMatcher ignoreCase() {....}
	   public static GenericPropertyMatcher caseSensitive() {....}
	   public static GenericPropertyMatcher contains() {....}
	   public static GenericPropertyMatcher endsWith() {....}
	   public static GenericPropertyMatcher startsWith() {....}
	   public static GenericPropertyMatcher exact() {....}
	   public static GenericPropertyMatcher storeDefaultMatching() {....}
	   public static GenericPropertyMatcher regex() {....}
        }	

      //Match modes for treatment of String values.
      enum StringMatcher {
           DEFAULT, //Store specific default
	   EXACT,//default if StringMatcher not applied
	   STARTING,
	   ENDING,
	   CONTAINING,
	   REGEX;//not yet supported (tested in version spring-data-jpa 2.1.1.RELEASE)
	}

	//Allows to transform the property value before it is used in the query
	interface PropertyValueTransformer extends Function<Optional<Object>, Optional<Object>> {....}
	....
}

We can divided above methods of ExampleMatcher in the following groups:

Applying and/or conjunction on non-null properties

  • 'and' conjunction' methods

    matching()/matchingAll()
  • 'or' conjunction method

    matchingAny();

Above methods are static factory methods, so they are starting point. The return value of ExampleMatcher can be chained further as all other methods also return ExampleMatcher.

Example

 Employee employee = new Employee();
 employee.setName("Diana");
 employee.setDept("IT");
 //it will return all Employees with name 'Diana' OR with dept 'IT'
 Example<Employee> employeeExample = Example.of(employee, ExampleMatcher.matchingAny());
 ....
 //it will return all Employees with name 'Diana' AND with dept 'IT'
 Example<Employee> employeeExample2 = Example.of(employee, ExampleMatcher.matchingAll());

Ignoring paths

withIgnorePaths(String... ignoredPaths)

Example

In following example, assuming Person entity's id is type of 'long' which will default to 0 instead of null. In that case we must ignore id property via ExampleMatcher:

 Person person = new Person();
 person.setName("Tara");
 ExampleMatcher exampleMatcher = ExampleMatcher.matching()//same as matchingAll()
                                               .withIgnorePaths("id");
 Example<Person> personExample = Example.of(person, exampleMatcher);

Case sensitivity

withIgnoreCase(),
withIgnoreCase(true/false),
withIgnoreCase(String... propertyPaths)

Example

 Employee employee = new Employee();
 employee.setName("tim");
 employee.setDept("qa");
 //ignoring case for all non-null properties 
 Example<Employee> employeeExample = Example.of(employee, ExampleMatcher.matchingAll().withIgnoreCase());
 //ignoring case for just 'name' property
 Example<Employee> employeeExample2 = Example.of(employee, ExampleMatcher.matchingAll().withIgnoreCase("name"));

Other String matching criteria

For all paths:

withStringMatcher(StringMatcher stringMatcher)

Example

 Employee employee = new Employee();
 employee.setName("k");
 ExampleMatcher matcher = ExampleMatcher.matchingAll()
                                        .withStringMatcher(ExampleMatcher.StringMatcher.ENDING);
 Example<Employee> employeeExample = Example.of(employee, matcher);

For specified path:

withMatcher(String propertyPath, GenericPropertyMatcher genericPropertyMatcher)
withMatcher(String propertyPath, MatcherConfigurer matcherConfigurer)

Example

Using first method:

 Employee employee = new Employee();
 employee.setName("k");
 employee.setDept("IT");
 ExampleMatcher name = ExampleMatcher.matchingAll()
                                     .withMatcher("name",
                                          ExampleMatcher.GenericPropertyMatchers.endsWith());
 Example<Employee> employeeExample = Example.of(employee, name);

Using second method:

 Employee employee = new Employee();
 employee.setName("k");
 employee.setDept("IT");
 ExampleMatcher name =
                ExampleMatcher.matchingAll()
                              .withMatcher("name",
                                      //can be replaced with lambda
                                      new ExampleMatcher.MatcherConfigurer<ExampleMatcher.GenericPropertyMatcher>() {
                                          @Override
                                          public void configureMatcher(ExampleMatcher.GenericPropertyMatcher matcher) {
                                              matcher.endsWith();
                                          }
                                      });
 Example<Employee> employeeExample = Example.of(employee, name);

Note that StringMatcher.REGEX is not yet supported (version spring-data-jpa 2.1.1.RELEASE). Check out source code: QueryByExamplePredicateBuilder#getPredicates()

Transforming property values

withTransformer(String propertyPath, PropertyValueTransformer propertyValueTransformer)

Example

 Employee employee = new Employee();
 employee.setName("Tim");
 employee.setDept("qa");
 ExampleMatcher matcher = ExampleMatcher.matchingAll()
                                        .withTransformer("dept", 
                //can be replaced with lambda  
                new ExampleMatcher.PropertyValueTransformer() {
                    @Override
                    public Optional<Object> apply(Optional<Object> o) {
                        if (o.isPresent()) {
                            return Optional.of(((String) o.get()).toUpperCase());
                        }
                        return o;
                    }
                });
 Example<Employee> employeeExample = Example.of(employee, matcher);

Handling nulls

withIncludeNullValues()
withIgnoreNullValues()
withNullHandler(NullHandler nullHandler)

Example

  Person person = new Person();
  //this will return all persons with null name
  ExampleMatcher exampleMatcher = ExampleMatcher.matchingAll()
                                               .withIgnorePaths("id")
                                               .withIncludeNullValues();
  Example<Person> employeePerson = Example.of(person, exampleMatcher);

Complete Example

Entities

@Entity
public class Employee {
  @Id
  @GeneratedValue
  private Long id;
  private String name;
  private String dept;
    .............
}
@Entity
public class Person {
  @Id
  @GeneratedValue
  private long id;
  private String name;
    .............
}

Repositories

public interface EmployeeRepository extends CrudRepository<Employee, Long>,
      QueryByExampleExecutor<Employee> {
}
public interface PersonRepository extends CrudRepository<Person, Long>,
      QueryByExampleExecutor<Person> {
}

Example Client

@Component
public class ExampleClient {

  @Autowired
  private EmployeeRepository exampleRepo;
  @Autowired
  private PersonRepository personRepo;

  public void run() {
      List<Employee> employees = createEmployees();
      exampleRepo.saveAll(employees);
      findAllEmployees();

      List<Person> persons = createPersons();
      personRepo.saveAll(persons);
      findAllPersons();

      findEmployeesByNameOrDept();
      findEmployeesByNameAndDept();
      findPersonByNameIgnoringIdPath();
      findEmployeeByNameAndDeptCaseInsensitive();
      findEmployeeByNameAndDeptIgnoringPathForCase();
      findEmployeeByNameEnding();
      findEmployeeByNameEndingAndByDept();
      //regex is not yet supported
      //findEmployeeByNameUsingRegex();
      findEmployeeByTransformingName();
      findPersonWithNullName();
      findEmployeeByNameIgnoringCaseAndContains();
  }

  private void findEmployeesByNameOrDept() {
      System.out.println("-- finding employees with name Diana or dept IT --");
      Employee employee = new Employee();
      employee.setName("Diana");
      employee.setDept("IT");
      System.out.println("Example entity: "+employee);
      Example<Employee> employeeExample = Example.of(employee, ExampleMatcher.matchingAny());
      Iterable<Employee> employees = exampleRepo.findAll(employeeExample);
      for (Employee e : employees) {
          System.out.println(e);
      }
  }

  private void findEmployeesByNameAndDept() {
      System.out.println("-- finding employees with name Diana and dept IT --");
      Employee employee = new Employee();
      employee.setName("Diana");
      employee.setDept("IT");
      System.out.println("Example entity: "+employee);
      Example<Employee> employeeExample = Example.of(employee, ExampleMatcher.matchingAll());
      Iterable<Employee> employees = exampleRepo.findAll(employeeExample);
      for (Employee e : employees) {
          System.out.println(e);
      }
  }

  private void findPersonByNameIgnoringIdPath() {
      System.out.println("-- finding person by name Tara ignoring id=0 --");
      Person person = new Person();
      person.setName("Tara");
      System.out.println("Example entity: "+person);
      ExampleMatcher exampleMatcher = ExampleMatcher.matching()
                                                    .withIgnorePaths("id");
      Example<Person> personExample = Example.of(person, exampleMatcher);
      personRepo.findAll(personExample).forEach(System.out::println);
  }

  private void findEmployeeByNameAndDeptCaseInsensitive() {
      System.out.println("-- finding employees with name tim and dept qa ignoring cases --");
      Employee employee = new Employee();
      employee.setName("tim");
      employee.setDept("qa");
      System.out.println("Example entity: "+employee);
      Example<Employee> employeeExample = Example.of(employee, ExampleMatcher.matchingAll().withIgnoreCase());
      Iterable<Employee> employees = exampleRepo.findAll(employeeExample);
      for (Employee e : employees) {
          System.out.println(e);
      }
  }

  private void findEmployeeByNameAndDeptIgnoringPathForCase() {
      System.out.println("-- finding employees with name tim (ignoring case) and dept QA (not ignoring case) --");
      Employee employee = new Employee();
      employee.setName("tim");
      employee.setDept("QA");
      System.out.println("Example entity: "+employee);
      Example<Employee> employeeExample = Example.of(employee, ExampleMatcher.matchingAll().withIgnoreCase("name"));
      Iterable<Employee> employees = exampleRepo.findAll(employeeExample);
      for (Employee e : employees) {
          System.out.println(e);
      }
  }

  private void findEmployeeByNameEnding() {
      System.out.println("-- finding employees with name ending k --");
      Employee employee = new Employee();
      employee.setName("k");
      System.out.println("Example entity: "+employee);
      ExampleMatcher matcher = ExampleMatcher.matchingAll()
                                             .withStringMatcher(ExampleMatcher.StringMatcher.ENDING);
      Example<Employee> employeeExample = Example.of(employee, matcher);
      Iterable<Employee> employees = exampleRepo.findAll(employeeExample);
      for (Employee e : employees) {
          System.out.println(e);
      }
  }

  private void findEmployeeByNameEndingAndByDept() {
      System.out.println("-- finding employees with name ending k and dept IT --");
      Employee employee = new Employee();
      employee.setName("k");
      employee.setDept("IT");
      System.out.println("Example entity: "+employee);
      
      ExampleMatcher name =
              ExampleMatcher.matchingAll()
                            .withMatcher("name",
                                    //can be replaced with lambda
                                    new ExampleMatcher.MatcherConfigurer<ExampleMatcher.GenericPropertyMatcher>() {
                                        @Override
                                        public void configureMatcher(ExampleMatcher.GenericPropertyMatcher matcher) {
                                            matcher.endsWith();
                                        }
                                    });
      Example<Employee> employeeExample = Example.of(employee, name);
      Iterable<Employee> employees = exampleRepo.findAll(employeeExample);
      for (Employee e : employees) {
          System.out.println(e);
      }
  }

  private void findEmployeeByNameUsingRegex() {
      System.out.println(" -- getting all Employees with name regex D.*a.*a --");
      Employee employee = new Employee();
      employee.setName("D.*a.*a");
      System.out.println("Example entity: "+employee);
      ExampleMatcher matcher = ExampleMatcher.matchingAll()
                                          .withStringMatcher(ExampleMatcher.StringMatcher.REGEX);
      Example<Employee> employeeExample = Example.of(employee, matcher);
      Iterable<Employee> employees = exampleRepo.findAll(employeeExample);
      for (Employee e : employees) {
          System.out.println(e);
      }
  }

  private void findEmployeeByTransformingName() {
      System.out.println(" -- getting all Employees with dept and transforming name during execution time  --");
      Employee employee = new Employee();
      employee.setName("Tim");
      employee.setDept("qa");
      System.out.println("Example entity: "+employee);
      ExampleMatcher matcher = ExampleMatcher.matchingAll().

              withTransformer("dept",
                      //can be replaced with lambda
                      new ExampleMatcher.PropertyValueTransformer() {
                          @Override
                          public Optional<Object> apply(Optional<Object> o) {
                              if (o.isPresent()) {
                                  return Optional.of(((String) o.get()).toUpperCase());
                              }
                              return o;
                          }
                      });
      Example<Employee> employeeExample = Example.of(employee, matcher);
      Iterable<Employee> employees = exampleRepo.findAll(employeeExample);
      for (Employee e : employees) {
          System.out.println(e);
      }
  }

  private void findPersonWithNullName() {
      System.out.println("-- finding person with null name --");
      Person person = new Person();
      person.setName(null);
      System.out.println("Example entity: "+person);
      ExampleMatcher exampleMatcher = ExampleMatcher.matchingAll()
                                                    .withIgnorePaths("id")
                                                    .withIncludeNullValues();
      Example<Person> personExample = Example.of(person, exampleMatcher);
      personRepo.findAll(personExample).forEach(System.out::println);
  }

  private void findEmployeeByNameIgnoringCaseAndContains() {
      System.out.println("-- finding employees with ignored case name contains 'AC' --");
      Employee employee = new Employee();
      employee.setName("AC");
      employee.setDept("IT");
      System.out.println("Example entity: "+employee);
      ExampleMatcher matcher = ExampleMatcher.matching()
                                           .withIgnoreCase("name")
                                           .withStringMatcher(ExampleMatcher.StringMatcher.CONTAINING);
      Example<Employee> employeeExample = Example.of(employee, matcher);
      Iterable<Employee> employees = exampleRepo.findAll(employeeExample);
      for (Employee e : employees) {
          System.out.println(e);
      }
  }

  private void findAllEmployees() {
      System.out.println(" -- getting all Employees --");
      Iterable<Employee> iterable = exampleRepo.findAll();
      for (Employee employee : iterable) {
          System.out.println(employee);
      }
  }

  private void findAllPersons() {
      System.out.println(" -- getting all Persons --");
      Iterable<Person> iterable = personRepo.findAll();
      for (Person person : iterable) {
          System.out.println(person);
      }
  }

  private static List<Employee> createEmployees() {
      return Arrays.asList(Employee.create("Diana", "IT"),
              Employee.create("Mike", "Admin"),
              Employee.create("Tim", "QA"),
              Employee.create("Jack", "IT"));
  }

  private static List<Person> createPersons() {
      return Arrays.asList(Person.create("Joe"),
              Person.create("Tara"),
              Person.create(null));
  }
}

Main class

public class ExampleMain {

  public static void main(String[] args) {
      AnnotationConfigApplicationContext context =
              new AnnotationConfigApplicationContext(AppConfig.class);
      ExampleClient exampleClient = context.getBean(ExampleClient.class);
      exampleClient.run();
      EntityManagerFactory emf = context.getBean(EntityManagerFactory.class);
      emf.close();
  }
}
 -- getting all Employees --
Employee{id=1, name='Diana', dept='IT'}
Employee{id=2, name='Mike', dept='Admin'}
Employee{id=3, name='Tim', dept='QA'}
Employee{id=4, name='Jack', dept='IT'}
-- getting all Persons --
Person{id=5, name='Joe'}
Person{id=6, name='Tara'}
Person{id=7, name='null'}
-- finding employees with name Diana or dept IT --
Example entity: Employee{id=null, name='Diana', dept='IT'}
Employee{id=1, name='Diana', dept='IT'}
Employee{id=4, name='Jack', dept='IT'}
-- finding employees with name Diana and dept IT --
Example entity: Employee{id=null, name='Diana', dept='IT'}
Employee{id=1, name='Diana', dept='IT'}
-- finding person by name Tara ignoring id=0 --
Example entity: Person{id=0, name='Tara'}
Person{id=6, name='Tara'}
-- finding employees with name tim and dept qa ignoring cases --
Example entity: Employee{id=null, name='tim', dept='qa'}
Employee{id=3, name='Tim', dept='QA'}
-- finding employees with name tim (ignoring case) and dept QA (not ignoring case) --
Example entity: Employee{id=null, name='tim', dept='QA'}
Employee{id=3, name='Tim', dept='QA'}
-- finding employees with name ending k --
Example entity: Employee{id=null, name='k', dept='null'}
Employee{id=4, name='Jack', dept='IT'}
-- finding employees with name ending k and dept IT --
Example entity: Employee{id=null, name='k', dept='IT'}
Employee{id=4, name='Jack', dept='IT'}
-- getting all Employees with dept and transforming name during execution time --
Example entity: Employee{id=null, name='Tim', dept='qa'}
Employee{id=3, name='Tim', dept='QA'}
-- finding person with null name --
Example entity: Person{id=0, name='null'}
Person{id=7, name='null'}
-- finding employees with ignored case name contains 'AC' --
Example entity: Employee{id=null, name='AC', dept='IT'}
Employee{id=4, name='Jack', dept='IT'}

Example Project

Dependencies and Technologies Used:

  • spring-data-jpa 2.1.1.RELEASE: Spring Data module for JPA repositories.
    Uses org.springframework:spring-context version 5.1.1.RELEASE
  • hibernate-core 5.3.7.Final: Hibernate's core ORM functionality.
    Implements javax.persistence:javax.persistence-api version 2.2
  • h2 1.4.197: H2 Database Engine.
  • JDK 1.8
  • Maven 3.5.4

ExampleMatcher examples Select All Download
  • spring-data-jpa-example-matchers
    • src
      • main
        • java
          • com
            • logicbig
              • example
                • ExampleClient.java
          • resources
            • META-INF

    See Also