Apart from JPQL constructor expressions, what other ways can a JPQL/HQL query return a non-entity shape — for example jakarta.persistence.Tuple or a Java record — and when would you choose each?
answer
- Object[] = positional, cast-heavy, local only
- Tuple = named access via aliases + column metadata
- record + createQuery(result class) = Hibernate 6 auto-instantiation
- native SQL → @SqlResultSetMapping / @ConstructorResult
- count → Long, sum(BigDecimal) → BigDecimal
basics
~20 sObject[] rows for a raw multi-column select; jakarta.persistence.Tuple for the same rows with named/typed accessors via aliases; and in Hibernate 6, passing a record or class to createQuery lets the provider instantiate it from a plain select list, without new. Constructor expressions remain the portable choice.
solid answer
~50 sFour options, roughly in order of increasing structure: - **`Object[]`** — `select o.id, o.total from Order o` typed as `Object[]`. Zero ceremony, no types, positional access. Fine inside a private method, poor as an API. - **`Tuple`** — `cb.createTupleQuery()` or `createQuery(hql, Tuple.class)`. Rows expose `get(0, Long.class)` and, when the select items are aliased (`o.total as total`), `get("total", BigDecimal.class)`. Useful when the column list is dynamic or generic code consumes it. - **Constructor expression** — `select new com.x.View(...)`, portable JPA, gives a real typed class. - **Hibernate 6 auto-instantiation** — `createQuery("select o.id, c.name from Order o join o.customer c", OrderView.class)` and Hibernate matches the select list to a constructor of the record/class; you can also write `select new OrderView(...)` without the package. Less noise, Hibernate-specific. Pick a typed class (record) for anything crossing a boundary; `Tuple` for generic or dynamically-shaped result handling; `Object[]` only locally. Native queries have their own route: `@SqlResultSetMapping` with `@ConstructorResult`.
code
java · 11 linesList<Tuple> rows = em.createQuery("""
select o.id as id, c.name as customer, sum(l.qty) as items
from Order o join o.customer c join o.lines l
group by o.id, c.name
""", Tuple.class).getResultList();
for (Tuple t : rows) {
Long id = t.get("id", Long.class);
String cust = t.get("customer", String.class);
Long items = t.get("items", Long.class);
}go deeper
Know that a multi-column select without new gives Object[], and that Tuple is the nicer typed alternative.
Explain Tuple's alias-based named access, Hibernate 6 record instantiation, and which to choose for a stable versus dynamic shape.
Add the native-query route via @SqlResultSetMapping/@ConstructorResult, portability trade-offs, and keeping JPA types out of outer layers.
Set the codebase convention for read-model shapes and decide how much Hibernate-specific convenience the project is willing to depend on.
## The shapes available ### Object[] Any multi-item select list without `new` yields `Object[]` per row: ```java List<Object[]> rows = em.createQuery( "select o.id, o.total from Order o", Object[].class).getResultList(); Long id = (Long) rows.get(0)[0]; ``` Untyped, positional, cast-heavy. Adding a column shifts every index downstream — a silent break. Acceptable inside a short private method that immediately maps to something better; never as a return type. ### jakarta.persistence.Tuple `Tuple` is the spec's structured answer to `Object[]`: still a row of values, but with type-checked and optionally *named* access. ```java List<Tuple> rows = em.createQuery( "select o.id as id, c.name as customer from Order o join o.customer c", Tuple.class).getResultList(); for (Tuple t : rows) { Long id = t.get("id", Long.class); String customer = t.get("customer", String.class); } ``` Names come from query aliases; without aliases only positional access works. `t.getElements()` exposes the column metadata, which is what makes `Tuple` the right fit for **generic** code — an export endpoint where the caller picks the columns, a reporting layer building the select list at runtime. In the Criteria API it is `cb.createTupleQuery()` plus `cq.multiselect(...)`, with `alias(...)` on each expression. The drawback: access is by string or index, so it is runtime-checked, and passing `Tuple` out of the persistence layer leaks a JPA type into the rest of the application. ### Constructor expressions Covered by the spec, portable, produce a real typed class. The default for anything with a stable shape crossing a layer boundary. ### Hibernate 6 instantiation from a plain select list Hibernate 6 removed most of the ceremony. Give `createQuery` a result class and it will match the select list against a constructor: ```java public record OrderView(Long id, String customer, BigDecimal total) {} List<OrderView> rows = session.createQuery( "select o.id, c.name, o.total from Order o join o.customer c", OrderView.class).getResultList(); ``` No `new`, no package name. Hibernate 6 also accepts `select new OrderView(...)` with just the simple name when the class is unambiguous, and can instantiate `Map` or `List` per row (`select new map(o.id as id, ...)`). Records are the natural target: their canonical constructor is positional and matches the select list directly, and immutability suits a read model. The cost is portability — this is Hibernate behaviour, not JPA. In a codebase already committed to Hibernate that is usually an acceptable trade, but say so out loud rather than assuming the spec guarantees it. ### Native queries A native SQL query cannot use JPQL constructor syntax. Two routes: `@SqlResultSetMapping` with a `@ConstructorResult` listing `@ColumnResult`s, declared on an entity and referenced by name from `createNativeQuery(sql, "MappingName")`; or `Tuple`/`Object[]` and map by hand. Hibernate additionally exposes result-transformer style APIs on `NativeQuery` for the same job. ## Choosing - **Stable shape, crosses a boundary** → typed record. Constructor expression for portability, Hibernate 6 auto-instantiation for brevity. - **Shape decided at runtime, or generic consumer** → `Tuple`. It is the only option that carries column names and metadata with the data. - **Two columns, consumed three lines later, inside one method** → `Object[]` is fine; do not build a class for it. - **Native SQL** → `@ConstructorResult` if reused, `Tuple` if one-off. ## Practical notes Watch the JPQL result types when writing constructors: `count` is `Long`, `sum` over integral types is `Long`, `sum` over `BigDecimal` stays `BigDecimal`, and date/time functions return the mapped temporal type. Alias every select item you plan to reach by name in a `Tuple`. And whichever shape you pick, all of them share the same property that matters most: the results are **unmanaged** — no persistence-context entry, no snapshot, no dirty checking, no flush involvement.
- When is Tuple the right choice over a typed DTO?When the select list is not known at compile time or the consumer is generic — a CSV export whose columns the caller chooses, an admin query builder, a reporting layer. Tuple carries the column names and types with the row via getElements(), which a fixed DTO cannot express. For a stable shape crossing a layer boundary a record is better: compile-time names, no string lookups, and no JPA type leaking outward.
- Does record instantiation from a plain select list work on any JPA provider?No — instantiating a result class from an unqualified select list, and the unqualified new Dto(...) form, are Hibernate 6 conveniences rather than JPA spec behaviour. Portable code uses a constructor expression with the fully qualified class name. If the project is committed to Hibernate the shorter form is fine, but it should be a stated choice rather than an assumption.
saying these in an interview costs you the question
- Returning Object[] or Tuple from a service or controller layer as the public shape
- Expecting Tuple.get("name") to work when the select item has no alias
- Assuming Hibernate 6 record auto-instantiation is guaranteed by the JPA spec
- Trying to use select new inside a native SQL query
- Declaring a DTO parameter as Integer for a count() or sum() that JPQL types as Long