skip to content

How does TypedQuery differ from the untyped Query interface in JPA, and what happens when getSingleResult() matches zero rows or more than one?

level: middleimportance: should knowfreq 52%

answer

  1. createQuery(jpql, Type.class) → TypedQuery<T>
  2. Type is declared, checked at runtime
  3. 0 rows → NoResultException
  4. 2+ rows → NonUniqueResultException
  5. getSingleResultOrNull (JPA 3.2) / uniqueResult

basics

~20 s

TypedQuery<T> carries the result type you pass to createQuery, so getResultList returns List<T> without a cast. getSingleResult throws NoResultException for zero rows and NonUniqueResultException for more than one; use getResultList or getSingleResultOrNull when absence is normal.

solid answer

~50 s

`em.createQuery(jpql, Book.class)` returns a `TypedQuery<Book>`, so `getResultList()` is `List<Book>` and `getSingleResult()` is `Book` — no casts, and a compile-time error if the class does not match what you then assign. The one-argument `createQuery(jpql)` returns a raw `Query` whose results are `Object`/`List`, which you must cast yourself. The type argument is a *declaration*, not a conversion: if the query actually selects two attributes, asking for `Book.class` fails at runtime. `getSingleResult()` is strict on both sides. Zero rows throws `NoResultException`; two or more throws `NonUniqueResultException`. Both are unchecked, and importantly both are `PersistenceException` subclasses — throwing a `PersistenceException` marks the surrounding transaction for rollback, so catching `NoResultException` as ordinary control flow is a trap. When "not found" is a normal outcome, prefer `getResultList()` and check for empty, or `getSingleResultOrNull()`, added in Jakarta Persistence 3.2 (Hibernate offered `uniqueResult()` long before). For multi-column projections use `Tuple.class`, or `Object[].class`, or a constructor expression.

code

java · 10 lines
java
Optional<Book> byIsbn(EntityManager em, String isbn) {
    return em.createQuery("select b from Book b where b.isbn = :isbn", Book.class)
             .setParameter("isbn", isbn)
             .setMaxResults(2)
             .getResultList()
             .stream()
             .findFirst();
}

Long total = em.createQuery("select count(b) from Book b", Long.class).getSingleResult();

go deeper

for a junior

Know that TypedQuery avoids casts and that getSingleResult throws when there is no row rather than returning null.

for a middle

Name both exceptions, match the result class to the projection, and prefer getResultList or getSingleResultOrNull for optional lookups.

for a senior

Discuss transaction-rollback semantics of PersistenceException, the cost of exception-driven control flow, and using setMaxResults(2) to bound and detect duplicates.

for a principal

Define the codebase convention for optional lookups and treat NonUniqueResultException as a missing database constraint rather than a case to handle.

## The two factory methods ```java TypedQuery<Book> typed = em.createQuery("select b from Book b", Book.class); Query raw = em.createQuery("select b from Book b"); List<Book> a = typed.getResultList(); // no cast List<?> b = raw.getResultList(); // Object elements ``` `TypedQuery<T>` exists purely so the API can hand back `List<T>` and `T` instead of raw types. It does not change execution, SQL, or mapping in any way. Every fluent method (`setParameter`, `setMaxResults`, `setHint`, `setFlushMode`, `setLockMode`) is overridden to return `TypedQuery<T>` so the type survives chaining. The type argument must be **compatible with what the query selects**, and the check is at runtime: - `select b from Book b` → `Book.class`. - `select b.title from Book b` → `String.class`. - `select count(b) from Book b` → `Long.class`. - `select b.title, b.price from Book b` → `Object[].class`, or `Tuple.class` (which gives named access via `tuple.get("title", String.class)` when aliases are declared), or a DTO type via a constructor expression. Passing `Book.class` to a query that selects two scalars raises an `IllegalArgumentException` when the query is created, not a `ClassCastException` later — which is precisely the value of using the typed form. ## Executing: three shapes - `getResultList()` — always returns a list, possibly empty. Never throws for "no rows". - `getSingleResult()` — expects exactly one row. - `getResultStream()` — a stream over results (JPA 2.2); by default it is materialised from the list unless the provider streams, and it must be closed if it holds resources. ## The two exceptions `getSingleResult()` throws: - `jakarta.persistence.NoResultException` when the query returns nothing; - `jakarta.persistence.NonUniqueResultException` when it returns more than one row. Both extend `PersistenceException` → `RuntimeException`, so nothing forces you to handle them. The subtlety that costs people production incidents: per the specification, a `PersistenceException` other than `NoResultException` and `NonUniqueResultException` causes the current transaction to be marked for rollback — and providers differ historically in how strictly they follow that carve-out. Even where the carve-out holds, using an exception for the common "row absent" path is expensive (stack trace construction) and obscures intent. ## What to write instead ```java // absence is normal List<Book> found = em.createQuery("select b from Book b where b.isbn = :isbn", Book.class) .setParameter("isbn", isbn) .setMaxResults(2) // detect duplicates cheaply .getResultList(); ``` Then branch on `found.isEmpty()`. Or, on Jakarta Persistence 3.2 and later, `getSingleResultOrNull()` returns `null` for zero rows and still throws `NonUniqueResultException` for many — the semantics most codebases actually want. Hibernate's native `org.hibernate.query.Query.uniqueResult()` has behaved that way for two decades, which is why Hibernate-native code often looks cleaner here. A related habit: when you expect at most one row but the schema does not enforce it, `NonUniqueResultException` is a *bug signal*, not a case to swallow. Add the unique constraint, or decide explicitly which row wins with an `ORDER BY` plus `setMaxResults(1)`. ## Interaction with the query itself `getSingleResult()` does not add a limit to the SQL — the provider fetches the rows and then complains. So a query that accidentally matches a million rows still transfers them before throwing. If a duplicate is plausible, use `setMaxResults(2)` with `getResultList()`, which caps the damage while still letting you detect the duplicate. ## Summary Use `TypedQuery` always; it costs one extra argument and removes a class of casts. Use `getResultList()` (or `getSingleResultOrNull()` where available) for lookups that may legitimately find nothing, and reserve `getSingleResult()` for invariants you are willing to treat as failures — such as loading a row you have just confirmed exists.

  • Which result class do you pass to createQuery for a query that selects two attributes?
    Object[].class for a plain tuple, or Tuple.class if you want named access through aliases declared in the select clause. Passing an entity class raises IllegalArgumentException at query-creation time, because the declared type is incompatible with the projection the query actually produces.
  • Why is catching NoResultException a poor way to express 'row may not exist'?
    It uses an exception for an expected outcome, which is costly and hides intent, and it sits inside the provider's exception hierarchy where transaction-rollback semantics are easy to get subtly wrong. Returning an empty list from getResultList, or null from getSingleResultOrNull, expresses absence as data rather than as failure.

saying these in an interview costs you the question

  • Believing TypedQuery converts or casts results rather than just declaring the type
  • Passing the entity class for a multi-attribute projection
  • Assuming getSingleResult returns null when nothing matches
  • Using NoResultException as normal control flow inside a transaction
  • Thinking getSingleResult adds a LIMIT so a huge match set is cheap

context