skip to content

How do you configure an ExampleMatcher for case-insensitive 'contains' search across some fields while ignoring others?

level: middleimportance: should knowfreq 45%

answer

  1. immutable — every with* returns a NEW matcher
  2. matching()=AND, matchingAny()=OR
  3. withStringMatcher(CONTAINING) + withIgnoreCase()
  4. withIgnorePaths for primitives / id / version
  5. withMatcher(path, m -> m.startsWith().ignoreCase()) per field

basics

~10 s

Use ExampleMatcher.matching() (AND) or matchingAny() (OR), then chain withIgnoreCase(), withStringMatcher(StringMatcher.CONTAINING), and withIgnorePaths("..."). You can also set per-field rules with withMatcher("name", m -> m.contains().ignoreCase()).

solid answer

~30 s

ExampleMatcher is immutable and fluent. Start with ExampleMatcher.matching() for AND semantics or matchingAny() for OR. Global settings: withStringMatcher(StringMatcher.CONTAINING) makes string fields use LIKE %value%, withIgnoreCase() makes them case-insensitive, withIgnorePaths("id","version") drops fields (essential for primitives), and withNullHandler / withIncludeNullValues controls null treatment. For per-property rules use withMatcher("firstName", m -> m.startsWith().ignoreCase()) or GenericPropertyMatchers.contains(). String matchers include DEFAULT, EXACT, STARTING, ENDING, CONTAINING, REGEX. Note case-insensitivity only applies to String properties and depends on the store honoring it. Every 'with*' call returns a new matcher instance, so you must chain or reassign — a common bug is calling withIgnoreCase() and discarding the result.

code

java · 12 lines
java
Person probe = new Person();
probe.setFirstName("an");
probe.setLastName("sm");

ExampleMatcher matcher = ExampleMatcher.matchingAny()   // OR across fields
        .withStringMatcher(ExampleMatcher.StringMatcher.CONTAINING) // LIKE %val%
        .withIgnoreCase()                                // case-insensitive strings
        .withIgnorePaths("id", "version", "active")       // drop primitives/tech fields
        .withMatcher("lastName", m -> m.startsWith().ignoreCase()); // per-field override

Example<Person> example = Example.of(probe, matcher);
List<Person> hits = personRepository.findAll(example);

go deeper

for a junior

May know matching() vs matchingAny() but likely misses the immutability trap.

for a middle

Should fluently configure string matcher, ignore-case, ignore-paths, and per-property matchers, and know each returns a new instance.

for a senior

Understands store-level case-folding caveats and when QBE's expressiveness runs out.

for a principal

Weighs QBE-based search UX against maintainability vs Querydsl for evolving filter requirements.

**`ExampleMatcher`** (`org.springframework.data.domain.ExampleMatcher`) is the immutable, fluent configuration that controls *how* a probe's properties are turned into predicates. Because it's immutable, **every `with...` method returns a new instance** — a frequent bug is `matcher.withIgnoreCase();` on its own line and then using the original `matcher`, which discards the change. **Creating a base matcher:** - `ExampleMatcher.matching()` — alias for `matchingAll()`; predicates combined with **AND**. - `ExampleMatcher.matchingAll()` — explicit AND. - `ExampleMatcher.matchingAny()` — predicates combined with **OR**. **Global (all-property) settings:** - `withStringMatcher(StringMatcher.CONTAINING)` — sets the default string-match mode. Options in `ExampleMatcher.StringMatcher`: `DEFAULT` (store default, usually exact), `EXACT`, `STARTING` (`value%`), `ENDING` (`%value`), `CONTAINING` (`%value%`), `REGEX`. - `withIgnoreCase()` — case-insensitive matching for **String** properties (no effect on non-strings). `withIgnoreCase("firstName", "lastName")` limits it to specific paths. - `withIgnorePaths("id", "version", "active")` — excludes properties entirely. Critical for **primitives** (which default to 0/false and would otherwise add predicates) and for auto-populated fields like id/version. - `withNullHandler(NullHandler.INCLUDE)` / `withIncludeNullValues()` — treat null probe fields as `IS NULL` predicates instead of ignoring them; `withIgnoreNullValues()` is the default. - `withTransformer(path, transformer)` — apply a `PropertyValueTransformer` to alter a value before it becomes a predicate. **Per-property settings** via `withMatcher(path, matcher)` using `ExampleMatcher.GenericPropertyMatcher` (built with `GenericPropertyMatchers` factory or lambda): ```java .withMatcher("firstName", m -> m.startsWith().ignoreCase()) .withMatcher("email", GenericPropertyMatchers.exact()) ``` Per-property matchers override the global string matcher/case setting for that path. **Semantics & gotchas:** - Case-insensitivity is only meaningful for strings and requires the underlying store to support `LOWER(...)`-style comparison; JPA generates it, but behavior can vary by database collation. - `matchingAny()` (OR) plus a probe with multiple set fields creates an OR across those fields — useful for 'search box hits any column'. - Matchers apply to the **root/simple properties**; deeply nested association matching is limited. - The matcher does **not** carry the probe; you combine them with `Example.of(probe, matcher)`. **When to use:** dynamic search forms with a fixed set of string-ish filters where you want AND/OR toggling and contains/starts-with without hand-writing queries. When you need ranges, IN, or complex boolean logic, switch to Querydsl or Specifications.

  • Why does calling matcher.withIgnoreCase() on its own line sometimes have no effect?
    ExampleMatcher is immutable; every with* method returns a new instance. If you don't capture the returned value (chain it or reassign the variable), the original unchanged matcher is used and the setting is lost.
  • What's the difference between the global withStringMatcher and a per-property withMatcher?
    withStringMatcher sets the default mode for all string properties; withMatcher(path, ...) overrides matching (mode/case) for one specific property, taking precedence over the global setting for that path.

saying these in an interview costs you the question

  • Assuming ExampleMatcher is mutable and setters apply in place
  • Thinking withIgnoreCase affects numeric fields
  • Forgetting withIgnorePaths for primitive fields so `active=false` sneaks in
  • Believing matchingAny() ANDs the predicates

context