skip to content

When a JPQL/HQL query selects a few columns, the result can be taken as an Object array, as a jakarta.persistence.Tuple, or as a DTO built by a constructor expression. How do these three shapes differ and when would you pick each?

level: middleimportance: should knowfreq 42%

answer

  1. Object[] = positional, cast, breaks on reorder
  2. single expression returns the value, not an array
  3. Tuple = access by alias + element metadata
  4. constructor expression / record = named typed fields
  5. all three unmanaged; same SQL either way

basics

~20 s

Object[] gives positional untyped values — fine for a throwaway query. Tuple adds access by alias and element type, still generic. A DTO or record gives named, typed fields checked by the compiler and is the right choice for anything crossing a service boundary.

solid answer

~50 s

All three run the same SQL; they differ only in how the row is handed to you. - **`Object[]`**: `createQuery("select u.id, u.name from User u")` yields arrays. Zero ceremony, but callers index by position and cast, so a reordered select list breaks them silently. Acceptable inside one method, never as a return type. - **`jakarta.persistence.Tuple`**: `createQuery(hql, Tuple.class)` gives `get("alias", Long.class)` plus element metadata. Useful when the select list is built dynamically (Criteria API, reporting), or in generic code that maps rows itself. Still untyped at compile time. - **DTO / record via constructor expression**: `select new com.app.UserSummary(u.id, u.name)`, or the type passed to `createQuery` with Hibernate 6.3+/JPA 3.2 implicit instantiation. Named, typed fields; refactoring-friendly; self-documenting. Default to the DTO. Reach for `Tuple` when the shape is not known at compile time, and keep `Object[]` for one-off internal queries. All three are unmanaged: none of them is dirty-checked or updatable.

code

java · 10 lines
java
List<Object[]> a = em.createQuery(
    "select u.id, u.name from User u", Object[].class).getResultList();

List<Tuple> b = em.createQuery(
    "select u.id as id, u.name as name from User u", Tuple.class).getResultList();
String name = b.get(0).get("name", String.class);

List<UserSummary> c = em.createQuery(
    "select new com.app.UserSummary(u.id, u.name) from User u",
    UserSummary.class).getResultList();

go deeper

for a junior

Know the three shapes exist and that a DTO or record is the readable default.

for a middle

Explain positional versus alias versus typed access, the single-expression gotcha, and that all three are unmanaged.

for a senior

Argue when dynamic shapes justify Tuple, and how to protect string-based constructor expressions with tests or the Criteria API.

for a principal

Treat it as a contract question: what type the read path publishes, how it evolves, and where generic row mapping is worth the loss of typing.

## Same SQL, different envelope Whichever shape you choose, the provider emits the same `select` for the same expression list. The choice is purely about the Java side: how values reach the caller and how much the compiler can help. ## Object[] ```java List<Object[]> rows = em.createQuery( "select u.id, u.name from User u", Object[].class).getResultList(); for (Object[] r : rows) { Long id = (Long) r[0]; } ``` Cheapest to write and completely opaque. Position is the contract, so adding a column in the middle of the select list breaks every consumer with a `ClassCastException` or, worse, a silently swapped pair of same-typed values. Note the single-expression special case: `select u.name from User u` returns `List<String>`, not `List<Object[]>`, which is a classic source of a cast error when someone later adds a second expression. Use it when the rows are consumed three lines below the query and never leave the method. ## jakarta.persistence.Tuple ```java List<Tuple> rows = em.createQuery( "select u.id as id, u.name as name from User u", Tuple.class).getResultList(); Long id = rows.get(0).get("id", Long.class); ``` `Tuple` is JPA's self-describing row: access by alias or index, with an expected type, and `getElements()` exposes the aliases and Java types. Two situations justify it: 1. **Dynamic select lists.** With the Criteria API, or a reporting endpoint where the caller picks the columns, there is no fixed class to instantiate. `Tuple` lets you build a `Map<String, Object>` per row generically. 2. **Generic mapping layers.** Code that converts rows to JSON or to a spreadsheet without knowing the shape. The costs: aliases are strings, so typos surface at runtime; the requested type is checked at runtime; and every consumer must know the alias vocabulary. It is a step up from `Object[]`, not a replacement for a real type. ## DTO via constructor expression ```java public record UserSummary(Long id, String name) {} List<UserSummary> rows = em.createQuery( "select new com.app.UserSummary(u.id, u.name) from User u", UserSummary.class).getResultList(); ``` The result is a named type with typed accessors. Consumers read `row.name()`, IDE navigation and refactoring work, and the class documents what the read path produces. Records are a particularly good fit: immutable, compact, canonical constructor matching the select order. With Jakarta Persistence 3.2 / Hibernate 6.3+ you may drop the `new` clause and let implicit instantiation match the select list against a constructor. One genuine weakness: because the query is a string, the constructor match is verified at query-compilation time, not by `javac`. Adding a parameter to the record without editing the query fails at runtime, so every projection query deserves a test that executes it. Criteria API users get a compile-checked variant with `cb.construct(UserSummary.class, root.get("id"), root.get("name"))`. ## Custom assembly When neither a constructor nor a tuple fits — you want to fold rows into a nested structure, for instance — Hibernate 6 offers `TupleTransformer` (per row) and `ResultListTransformer` (whole list), which replaced the removed `ResultTransformer` interface from Hibernate 5. That is also where you land when a single query returns parent columns repeated across child rows and you need to collapse them. ## The property they share None of these results is managed. There is no persistence-context entry, no loaded-state snapshot, no dirty check, no lazy loading. That is the point of a projection, and it is what makes the choice among the three a pure ergonomics decision rather than a performance one. If someone claims `Tuple` is faster than a DTO, ask them to explain the mechanism — there is none worth measuring; the SQL and the row count are identical. ## Rule of thumb DTO or record by default; `Tuple` when the shape is dynamic or the mapping is generic; `Object[]` only for a query whose results never escape the enclosing method.

  • What does a JPQL query with exactly one selected expression return, and why does that trip people up?
    It returns the values themselves — `select u.name from User u` yields `List<String>`, not `List<Object[]>`. Code written against the single-column form breaks with a `ClassCastException` the moment someone adds a second expression to the select list, which is a good argument for using a DTO from the start.
  • Is there a way to get compile-time checking of a projection's shape?
    Yes, with the Criteria API: `cb.construct(UserSummary.class, root.get("id"), root.get("name"))` is ordinary Java, so the class reference is checked by the compiler, and with the generated static metamodel the attribute names are checked too. String-based JPQL constructor expressions are only validated when the query is compiled at runtime, so back them with a test.

saying these in an interview costs you the question

  • Returning `List<Object[]>` from a service method and letting callers index it
  • Assuming a one-expression select returns `Object[]` of length one
  • Claiming `Tuple` is faster or slower than a DTO — the SQL is identical
  • Thinking `Tuple` results are managed or updatable
  • Using `Tuple` everywhere because "it avoids writing classes", then re-deriving the shape in every caller

context