What are the key limitations and gotchas of Query by Example around primitives, null handling, and nested/associated properties?
answer
- primitives default to 0/false → silent predicate → withIgnorePaths
- null object fields ignored, not IS NULL (opt in with includeNullValues)
- string matchers/ignore-case = strings only
- no ranges / IN / NOT / mixed AND-OR
- nested matching limited; no collection-association match
basics
~20 sPrimitive fields can't be null, so their default value (0/false) silently becomes a filter — ignore those paths. Null object fields are skipped by default (not matched as IS NULL). QBE also can't do ranges, and matching on nested associations is limited.
solid answer
~40 sQBE reads the probe's properties reflectively. Object-type fields left null are ignored by default (NullHandler.IGNORE); to match them as IS NULL you must opt in with withIncludeNullValues(). Primitives (int, boolean, long) can never be null, so they always carry a default (0, false) and QBE adds a predicate for them unless you withIgnorePaths(...) — a silent, hard-to-spot bug. String matching modes (contains, starts-with) and ignore-case apply to strings only. Beyond that, QBE fundamentally can't express ranges (<, >, BETWEEN), IN with a collection, negation, or arbitrary OR/AND nesting. Nested/associated property matching works only for populated nested probe objects and is limited; you can't, e.g., match 'any element of a collection association'. When these limits bite, move to Specifications or Querydsl.
code
java · 13 lines// Entity with a primitive - danger zone
class Person { String name; int age; boolean active; /* ... */ }
Person probe = new Person();
probe.setName("Ann");
// age defaults to 0, active defaults to false -> both become predicates!
ExampleMatcher matcher = ExampleMatcher.matching()
.withIgnorePaths("age", "active") // MUST ignore primitives you didn't set
.withStringMatcher(ExampleMatcher.StringMatcher.CONTAINING)
.withIgnoreCase(); // affects 'name' (String) only
List<Person> hits = personRepository.findAll(Example.of(probe, matcher));go deeper
May know non-null fields are matched but miss the primitive/null subtleties.
Should name the primitive gotcha, default null-ignore behavior, and the range/IN limitation.
Explains store-dependence and cleanly maps each limitation to a Specifications/Querydsl alternative.
Uses these limits to set team guidance on when QBE is acceptable versus mandating a richer DSL.
Query by Example is convenient but has sharp edges rooted in how it introspects the **probe**. ## Nulls and primitives **1. Null handling (object types):** By default `ExampleMatcher` uses `NullHandler.IGNORE` — a `null` property contributes **no predicate**. So a null `lastName` means 'don't filter on lastName', *not* 'lastName IS NULL'. To search for actual nulls you must switch to `withIncludeNullValues()` (or `withNullHandler(NullHandler.INCLUDE)`), which emits `IS NULL` for null probe fields. **2. Primitive gotcha (the big one):** Java primitives (`int`, `long`, `boolean`, `double`) **cannot be null**. When you `new` a probe, they default to `0` / `false`. QBE can't tell 'unset' from 'deliberately 0', so it **always adds a predicate** like `age = 0` or `active = false`. This silently over-constrains the query and returns few/no rows. Fixes: - (a) `matcher.withIgnorePaths("age", "active")`, or - (b) use **boxed types** (`Integer`, `Boolean`) in the entity so unset fields are null and thus ignored. ## What QBE can and cannot match **3. String-only options:** `withStringMatcher(...)` (EXACT/STARTING/ENDING/CONTAINING/REGEX) and `withIgnoreCase()` only affect **String** properties. Numbers/dates are always exact-equality — reinforcing that ranges are impossible. **4. No range / set / boolean-logic support:** QBE emits only `=` and string `LIKE`. It **cannot** express: - `age > 18`, - `salary BETWEEN a AND b`, - `status IN (...)`, - `NOT`, - or arbitrary mixes of AND and OR (it's all-AND via `matching()` or all-OR via `matchingAny()`, not per-branch grouping). ## Nested properties and store dependence **5. Nested / associated properties:** You can populate a nested probe object (e.g., `probe.getAddress().setCity("Berlin")`) and QBE will match on the traversed simple property, but support is **limited**: matching *inside collection associations* (e.g., 'has an order with total > X') isn't expressible, and behavior on deep graphs varies. Ignore-case/string-matcher on nested paths must be configured per full path. **6. Store dependence:** QBE for JPA builds Criteria predicates; some matchers (regex, certain case handling) depend on the underlying store/database. QBE is also available for MongoDB/others with slightly different capabilities. ## When these limits bite Switch to either of: - **Specifications** (`JpaSpecificationExecutor`) or - **Querydsl** (`QuerydslPredicateExecutor`), both of which handle ranges, IN, negation, OR-grouping, and richer association queries.
- Two ways to avoid the primitive-field gotcha?1) Call matcher.withIgnorePaths("age", "active") to exclude the unset primitive paths. 2) Model those fields as boxed types (Integer, Boolean) in the entity so an unset value is null and QBE ignores it under the default NullHandler.IGNORE.
- How do you make QBE actually match rows where a column IS NULL?Configure the matcher with withIncludeNullValues() (or withNullHandler(NullHandler.INCLUDE)). Then null properties on the probe emit IS NULL predicates instead of being skipped.
saying these in an interview costs you the question
- Believing a null probe field matches IS NULL by default
- Not realizing primitive defaults silently add predicates
- Thinking QBE can filter a collection association like 'orders with total > 100'
- Assuming ignore-case affects numeric equality