skip to content

DTO Projections

Selecting exactly the columns a read path needs into constructor-built DTOs instead of loading managed entities. Interviewers treat select-new projections as the correct default answer for read-heavy endpoints.

part ofHibernateoverview, primer and where to startread it →
on this pageshow

questions

4

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?

level: juniorimportance: must knowfreq 55%

answer

  1. select new com.pkg.Dto(...)
  2. FQCN in classic JPA; Hibernate 6 relaxes
  3. constructor matched positionally by type
  4. result is unmanaged — no snapshot, no dirty check
  5. SQL selects only the listed columns

basics

~20 s

Write 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 s

JPQL 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 lines
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.status = :status
        """, OrderView.class)
    .setParameter("status", Status.NEW)
    .getResultList();

go deeper

for a junior

Write the select new syntax correctly, name the fully qualified class and matching public constructor, and say the result is a plain unmanaged object.

for a middle

Add that only the listed columns are selected, that count/sum are Long, and the Criteria cb.construct equivalent.

for a senior

Discuss the read-model boundary — no snapshot or dirty checking — and the to-many limitation with its workarounds.

for a principal

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

context

open as a page

For a read-only list screen, why would you project query results into DTOs rather than loading the mapped entities and reading their fields?

level: middleimportance: must knowfreq 50%

basics

~20 s

Loading entities selects every mapped column, puts each object in the persistence context, and stores a snapshot copy for dirty checking that Hibernate must compare at flush. A DTO query selects only the needed columns, allocates one small object per row, and is never dirty-checked or flushed.

open as a page

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?

level: middleimportance: should knowfreq 32%

basics

~20 s

Object[] 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.

open as a page

You need one row per order carrying the customer's name and the number of line items on that order. How do you shape that as a JPQL DTO projection, and what goes wrong when a projection spans a to-many association?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Join the to-one customer flatly and aggregate the to-many: select new View(o.id, c.name, count(l)) from Order o join o.customer c left join o.lines l group by o.id, c.name. Without the aggregate, joining a to-many multiplies rows — one per line item — so the DTO is instantiated repeatedly per order.

open as a page