skip to content

An entity maps a boolean to the characters 'Y' and 'N' through a JPA AttributeConverter. What can go wrong when you query, sort or index that column, and how do you work around it?

level: seniorimportance: should knowfreq 32%

answer

  1. converter = bind/extract only
  2. JPQL parameters converted; native SQL not
  3. order by / like / functions see the stored form
  4. null→non-null conversion breaks IS NULL
  5. opaque stored form = unfilterable, unindexable

basics

~20 s

Conversion happens at parameter binding and result extraction only. Bound query parameters are converted; native SQL is not, and any database-side expression — sorting, LIKE, functions, index order — sees the stored form ('Y'/'N'), not the Java value.

solid answer

~60 s

A converter is a binding-time function, so its reach is limited to values that pass through the mapping. **What works:** JPQL and Criteria queries with *bound parameters* on a converted attribute — the provider converts the parameter before binding, so `where active = :flag` with `true` becomes `= 'Y'`. **What does not:** native SQL bypasses the mapping entirely, so you must write the stored form yourself. Anything the database evaluates — `order by`, `like`, `upper()`, ranges, `min`/`max`, index ordering — operates on the stored representation. `'Y' < 'N'` is false, so ordering by the flag orders by letter, not by truth. For an enum stored as a code, ordering follows the code, not the enum's declaration order. **Also:** `IS NULL` semantics shift if the converter turns Java `null` into a non-null value; and a converter that stores an opaque blob or JSON string makes the column unfilterable and unindexable in any useful way. The rules that follow: bind parameters rather than writing literals, choose a stored form whose natural ordering and comparison match the domain, and never convert a column you need to range-scan into an opaque form.

code

java · 9 lines
java
// converted: true -> 'Y' bound into the statement
em.createQuery("select o from Order o where o.active = :flag", Order.class)
  .setParameter("flag", true);

// NOT converted: native SQL takes the stored form verbatim
em.createNativeQuery("select * from orders where active = 'Y'", Order.class);

// sorts by the character, not by the boolean's meaning
em.createQuery("select o from Order o order by o.active", Order.class);

go deeper

for a junior

Know that a converter runs in Java when values are written and read, not inside the database.

for a middle

Explain that bound parameters are converted while native SQL is not, and that database-side operations see the stored form.

for a senior

Work through ordering, pattern and range predicates, aggregates, index usability and null semantics, and state the design rules that avoid the problem.

for a principal

Treat the stored form as the column's public contract for reporting and other consumers, and set policy on which attributes may be converted at all given query and integration needs.

## Where a converter sits A converter is invoked in exactly two places: when the provider binds a value into a JDBC statement, and when it extracts one from a result set. Everything else in the stack — the SQL text, the database's evaluation of predicates, the index, the sort — deals with the *stored* form and knows nothing about the Java type. Once you hold that model, every surprise in this area is predictable. ## Parameters are converted; the database's own work is not A JPQL query comparing a converted attribute to a bound parameter works as you expect: ```java em.createQuery("select o from Order o where o.active = :flag", Order.class) .setParameter("flag", true) .getResultList(); ``` The provider knows `o.active` is converted, so it runs `true` through `convertToDatabaseColumn` and binds `'Y'`. The Criteria API behaves the same way, since it also goes through parameter binding. Inline **literals** are the shakier case. Historically, providers did not push literals in query text through the converter, so `where o.active = true` compiled to a comparison against a boolean the column cannot hold, producing either a type error or silently wrong results. Modern Hibernate handles many of these cases, but the version dependence is exactly why the durable advice is: **bind parameters, do not write literals** against converted attributes. ## Native SQL bypasses conversion for input A native query is text you hand to the database. Nothing in it is converted, so you must write the stored form: `where active = 'Y'`. This cuts both ways — results mapped back onto entities *are* converted on extraction, because that path goes through the mapping, but scalar projections come back raw. The asymmetry catches people who convert one direction and forget the other. Bulk `update`/`delete` statements deserve the same care: in JPQL they bind parameters and convert, but they bypass the persistence context, and in native form they bypass conversion too. ## Database-side evaluation sees the stored form This is the substantial category: - **Ordering.** `order by o.active` sorts `'N'` before `'Y'`, which happens to match `false` before `true` — a coincidence. Store a status enum as `'A'`, `'P'`, `'X'` and `order by status` gives you alphabetical order, not lifecycle order. If a report depends on domain order, either store an order-preserving code or sort in a `case` expression. - **Range and pattern predicates.** `like`, `between`, `>` and `<` all mean whatever they mean over the stored form. A `Money` converted to `BigDecimal` ranges correctly; a `Money` converted to the string `"12.34 EUR"` does not. - **Functions.** `upper(o.code)` runs on the stored string; if the converter also uppercases, you have two places doing the same thing, and if it does not, case handling differs between Java and SQL paths. - **Aggregates.** `min`/`max`/`avg` are computed by the database on the stored type, then extracted and converted; an aggregate over a converted type may not even be extractable back into the Java type. - **Grouping and `distinct`** collapse on the stored form, so two Java values that convert to the same stored value become one group. ## Indexes Indexes are unaffected in the sense that they index the stored column normally — a `char(1)` index over `'Y'`/`'N'` works, though its selectivity is poor. The real damage comes from converters that make the column opaque: a value object serialized to JSON or to a delimited string cannot be range-scanned, and a predicate over one of its parts requires either a full scan with a database-side function or a vendor-specific expression index. If you filter on it, do not hide it inside a converted blob. ## Nulls If `convertToDatabaseColumn(null)` returns a non-null value — say an empty string, or `'N'` for a null Boolean — then `where o.active is null` will never match a row that was null in Java. Conversely, a converter that maps a sentinel stored value back to Java `null` creates rows the database does not consider null. Decide the null contract deliberately and document it, because query semantics hinge on it. ## Design rules 1. Choose a stored form whose natural comparison and ordering match the domain's, or accept that ordering must be expressed explicitly. 2. Bind parameters; avoid literals in JPQL over converted attributes. 3. Treat native SQL as a separate world and write the stored form there by hand — and keep the mapping between the two forms in one place, so it can change in one place. 4. Never convert a column you need to filter, range-scan or index into an opaque or composite representation. Use separate columns or an embeddable instead. 5. Watch out for reporting and ad-hoc SQL by other teams: converters are invisible outside your application, so the stored form is the real public contract of that column.

  • Does a JPQL query with a bound parameter on a converted attribute work correctly?
    Yes. The provider knows the attribute is converted and runs the parameter through convertToDatabaseColumn before binding, so the SQL compares against the stored form. The Criteria API behaves identically because it also goes through parameter binding. Inline literals are the unreliable case and native SQL is not converted at all.
  • An enum is stored as a short code through a converter, and a report must order by lifecycle stage. What are the options?
    Either choose a stored form whose natural sort order matches the lifecycle — for example numeric codes assigned in stage order — or express the order explicitly in the query with a case expression mapping each stored code to a rank. Sorting in memory after loading is a third option but only works when the result set is small and unpaginated. What does not work is assuming order by on the attribute reflects the enum's declaration order.

saying these in an interview costs you the question

  • Assuming converters apply to native SQL statements.
  • Expecting order by on a converted attribute to follow the Java type's ordering.
  • Storing a composite or serialized form in a column that must be filtered or range-scanned.
  • Ignoring that a null-to-non-null conversion breaks IS NULL predicates.
  • Believing conversion happens inside the database.

context