How do you make a JPQL query return instances of a plain DTO class instead of mapped entities using a constructor expression, and what must the DTO class provide for it to work?
answer
- select new com.pkg.Dto(...)
- FQCN in classic JPA; Hibernate 6 relaxes
- constructor matched positionally by type
- result is unmanaged — no snapshot, no dirty check
- SQL selects only the listed columns
basics
~20 sWrite select new com.example.OrderView(o.id, o.total, o.customer.name) from Order o. The class name must be fully qualified, and the class needs a public constructor whose parameter types and order match the selected expressions. Results come back as plain, unmanaged objects.
solid answer
~50 sJPQL constructor expressions instantiate an arbitrary class per result row: ```java List<OrderView> rows = em.createQuery( "select new com.example.OrderView(o.id, o.total, c.name) " + "from Order o join o.customer c where o.status = :s", OrderView.class) .setParameter("s", Status.NEW) .getResultList(); ``` Requirements: - **Fully qualified class name** after `new` (in classic JPA; Hibernate 6 relaxes this when the query's result class is passed to `createQuery`). - A **public constructor** whose parameter types match the selected expressions in **order** and count; the provider matches by signature, not by name. - The class does **not** need to be an entity, need not be mapped, and needs no no-arg constructor. The returned objects are plain Java objects — **not managed**. They are never dirty-checked, never flushed, and take no space in the persistence context beyond the list itself. The SQL selects only the listed columns, so you read the four columns the screen needs rather than every column of the entity. The Criteria equivalent is `cb.construct(OrderView.class, ...)`.
code
java · 10 linespublic record OrderView(Long id, BigDecimal total, String customerName) {}
List<OrderView> rows = em.createQuery("""
select new com.example.OrderView(o.id, o.total, c.name)
from Order o
join o.customer c
where o.status = :status
""", OrderView.class)
.setParameter("status", Status.NEW)
.getResultList();go deeper
Write the select new syntax correctly, name the fully qualified class and matching public constructor, and say the result is a plain unmanaged object.
Add that only the listed columns are selected, that count/sum are Long, and the Criteria cb.construct equivalent.
Discuss the read-model boundary — no snapshot or dirty checking — and the to-many limitation with its workarounds.
Frame DTO projections as the default read path and set the codebase rule for where entity loading is actually required.
## What a constructor expression is A JPQL query normally returns managed entities or scalar values. A **constructor expression** — `select new <FQCN>(expr, expr, ...)` — tells the provider to call a Java constructor once per result row, passing the selected expressions as arguments. The result list contains ordinary objects of your class. ```java public record OrderView(Long id, BigDecimal total, String customerName) {} List<OrderView> rows = em.createQuery( "select new com.example.OrderView(o.id, o.total, c.name) " + "from Order o join o.customer c " + "where o.createdAt >= :since", OrderView.class).setParameter("since", since).getResultList(); ``` ## The rules **Fully qualified name.** Classic JPA requires the whole package path in the query string. It is verbose and it is why people put such queries in named queries or constants. Hibernate 6 lets you write just `new OrderView(...)` — or omit `new` entirely — when the result class is given to `createQuery`. **Matching constructor.** The provider resolves a constructor by the number and types of the selected expressions, in order. Getting the order wrong between two same-typed parameters compiles fine and silently swaps values at runtime — a real bug class; guard it with a test. Widening applies (an `Integer` argument can feed a `long` parameter), but there is no coercion between unrelated types. Note the types JPQL produces: `count(...)` is `Long`, `sum` over an integral type is `Long`, and a `sum` over `BigDecimal` is `BigDecimal` — constructors must accept those exact widths, and a `sum` typed as `int` is the classic "no matching constructor" failure. **Nothing special about the class.** It need not be an entity, need not be in the persistence unit, need not have a no-arg constructor, and needs no annotations. Records work well precisely because their canonical constructor is positional. A class must not be an inner (non-static) class. **Nesting.** JPA 3.2 and Hibernate allow a nested `new` inside a constructor expression, so a DTO can hold a sub-DTO. Use sparingly — readability drops fast. ## What the SQL looks like The generated SQL selects **only the listed columns**: ```sql select o.id, o.total, c.name from orders o join customers c on c.id = o.customer_id where o.created_at >= ? ``` Compare with `select o from Order o`, which selects every mapped column of `orders` (and every column of any eagerly mapped to-one). On a wide table with large text or blob columns, that difference alone can dominate the query's cost. ## Why the result being unmanaged matters Entities returned by a query are put into the persistence context, and Hibernate keeps a **snapshot** of each one so it can detect changes at flush. A DTO gets none of that: no snapshot, no dirty check, no flush participation, no cascade, no second-level cache interaction, no risk of an accidental write because some code touched a setter. For a read-only screen that is all upside. It also means DTOs cannot be modified and saved — they are read models, and updating requires loading the entity. ## Where it falls short - **No collections.** A constructor expression produces one object per SQL row, so it cannot build a DTO holding a list of children. Joining a to-many gives you duplicate parent rows, one per child. Options: a second query for the children keyed by parent id, an aggregate in the projection (`count(l)` with `group by`), or Hibernate's `ResultTransformer`-style post-processing. - **Positional fragility.** Two `String` parameters in the wrong order is a silent bug. - **Verbosity** of the fully qualified name in classic JPA. ## Alternatives to know `jakarta.persistence.Tuple` returns rows as a labelled bag of values without any DTO class; Hibernate 6 can instantiate records directly from a `select id, name ...` list when the result class is passed in; and native queries can be mapped to DTOs through `@SqlResultSetMapping` with a `@ConstructorResult`. Constructor expressions remain the portable default. ## In Criteria form ```java cq.select(cb.construct(OrderView.class, o.get("id"), o.get("total"), c.get("name"))); ``` Same semantics, same constructor-matching rules.
- Can a constructor expression populate a DTO that holds a list of child DTOs?Not directly — a constructor expression runs once per SQL row, and joining a to-many yields one row per child, so the parent would be instantiated repeatedly. The usual solutions are a second query fetching children by parent id and grouping them in Java, an aggregate such as count(child) in the projection with a group by, or a post-processing pass over a flat projection. Some codebases use Hibernate's result-transformer style APIs for the grouping step.
- Your DTO takes an int for a count but the query uses count(l). Why does it fail, and how do you fix it?JPQL's count() is typed as Long, so the provider looks for a constructor accepting Long and finds none, failing with a no-matching-constructor error. Fix it by declaring the DTO parameter as Long (preferred), or by casting in the query. The same trap appears with sum over integral columns, which is also Long, and with sum over BigDecimal columns, which stays BigDecimal.
- Do objects returned from a constructor expression participate in the persistence context?No. They are plain unmanaged objects: Hibernate keeps no loaded-state snapshot for them, they are not dirty-checked, they never take part in a flush, and modifying one has no effect on the database. That is precisely why they are the right shape for read-only screens, and why you must load the entity if you intend to update something.
saying these in an interview costs you the question
- Thinking the DTO must be an entity or need a no-arg constructor
- Expecting the provider to match constructor arguments by name rather than by position and type
- Believing a constructor expression still selects all entity columns under the hood
- Trying to modify a projected DTO and expecting the change to be persisted
- Declaring a count(...) result as int or Integer in the DTO constructor