skip to content

Entity vs DTO Reads

Why managed entities are the wrong vehicle for read-only traffic — snapshot memory, dirty-check CPU, L1 growth — and DTO projection as the read-path default. A framing interviewers use to test whether you separate write models from read models.

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

questions

5

In JPA/Hibernate, how do you make a JPQL/HQL query return instances of your own DTO class instead of managed entity objects, and what must that class provide?

level: juniorimportance: must knowfreq 62%

answer

  1. select new com.app.Dto(...)
  2. fully qualified name, matching constructor
  3. no collections in arguments
  4. results are unmanaged — writes do nothing
  5. records + implicit instantiation (JPA 3.2 / HB 6.3)

basics

~20 s

Use a constructor expression: select new com.app.UserDto(u.id, u.name) from User u. Name the class fully qualified and give it a constructor whose parameter types match the selected expressions in order. Results are plain objects, not managed entities.

solid answer

~50 s

The portable way is a **constructor expression** in JPQL/HQL: ``` select new com.app.UserDto(u.id, u.name) from User u where u.active = true ``` Rules: the class is written with its **fully qualified name**, it needs a **public constructor whose parameter types and order match the select list**, and the select list holds scalar expressions only — you cannot pull a whole collection into a constructor argument. Execute it with `em.createQuery(hql, UserDto.class).getResultList()`. Jakarta Persistence 3.2 / Hibernate 6.3+ also support *implicit instantiation*: pass the DTO type to `createQuery` and, when the select list matches a constructor, the `new` clause can be omitted; Java **records** work as DTOs. The semantics matter more than the syntax: the returned objects are **not managed**. They have no persistence-context entry and no dirty checking, so mutating one persists nothing — which is exactly why projections are cheap on read paths.

code

java · 7 lines
java
public record UserSummary(Long id, String name, String city) {}

List<UserSummary> rows = em.createQuery(
        "select new com.app.UserSummary(u.id, u.name, u.address.city) " +
        "from User u where u.active = true",
        UserSummary.class)
    .getResultList();

go deeper

for a junior

Recall the syntax, the fully-qualified-name and matching-constructor rules, and that the results are not managed.

for a middle

Add why it is faster — narrower select, no persistence-context entry, no dirty checking — and the collection limitation with the two-query workaround.

for a senior

Discuss where DTOs belong in a read path, testing string queries, one DTO per use case, and when an entity read is still the right call.

for a principal

Frame it as read-model design: query-shaped types owned by the endpoint, kept out of the write model, and the maintenance cost of many narrow projections.

## What a projection is `select u from User u` tells the provider to select every mapped column of the entity, build a `User` instance, and register it in the persistence context as a **managed** object. A *projection* selects individual expressions instead — `u.id`, `u.name`, `count(o)` — and hands you the values. A **DTO projection** wraps those values in a class you control, one shaped for the caller (an HTTP response, a report row) rather than for the database table. ## Constructor expressions The standard mechanism is the constructor expression, part of JPQL since JPA 1.0: ``` select new com.app.UserSummary(u.id, u.name, u.address.city) from User u where u.active = true ``` Mechanically, the provider emits SQL selecting exactly those three columns, then for each row calls `new UserSummary(...)` with the converted values. Requirements: - **Fully qualified class name.** JPQL has no import mechanism, so `new UserSummary(...)` fails unless the provider is configured to resolve short names (Hibernate 6 allows registering imports; do not rely on it in portable code). - **A matching constructor.** Parameter count, order, and assignable types must line up. A `Long` id against an `int` parameter or two swapped `String` parameters is a runtime failure, not a compile-time one — the query is a string. - **Scalar arguments only.** You can pass entity-valued paths in Hibernate, but you cannot pass a collection: `new Dto(u.id, u.orders)` is invalid. Nested one-to-many data needs either a second query keyed by the parent ids, or Hibernate 6's `multiset()` HQL function. - The DTO does not need to be an entity, does not need a no-arg constructor, and should not be annotated with `@Entity`. ## Newer conveniences Jakarta Persistence 3.2 and Hibernate 6.3+ added **implicit instantiation**: `em.createQuery("select u.id, u.name from User u", UserSummary.class)` will find a constructor matching the select list. Java **records** are ideal DTO carriers — the canonical constructor matches the component order, and immutability suits a read model. Hibernate also offers `jakarta.persistence.Tuple` and raw `Object[]` when you do not want a class per query, and Hibernate 6 replaced the old `ResultTransformer` with `TupleTransformer`/`ResultListTransformer` for custom assembly. ## Why it matters: the results are detached values The objects a constructor expression produces are ordinary Java objects. Hibernate does not put them in the persistence context, does not keep a loaded-state snapshot of them, and never dirty-checks them at flush. Consequences: - Changing a field on the DTO changes nothing in the database. Candidates who expect an update here misunderstand the model. - There is no lazy loading. Anything not in the select list is simply absent; you cannot navigate from a DTO to an unselected association, and you will never see a lazy-initialization failure from one. - To *write*, you must load the entity by id (or run a bulk `update` statement). A DTO is a read artifact. ## A worked contrast ```java // managed read: all columns, entity in the persistence context, snapshot retained List<User> users = em.createQuery("from User u", User.class).getResultList(); // DTO read: two columns, nothing retained List<UserSummary> rows = em.createQuery( "select new com.app.UserSummary(u.id, u.name) from User u", UserSummary.class) .getResultList(); ``` The second form usually emits a narrower `select`, transfers fewer bytes, allocates one small object per row instead of an entity plus its state snapshot, and adds nothing to later flush cost. ## Practical notes Keep one DTO per read use case rather than a shared "wide" DTO that every query half-fills — half-populated DTOs are as confusing as partially initialised entities. Because the query is a string, cover each constructor expression with a test that actually executes it; a renamed constructor parameter type is otherwise found in production. And name the DTO for the *view* it serves (`UserSummary`, `InvoiceRow`), not for the entity, so nobody mistakes it for a second mapping of the table.

  • If I change a field on one of the returned DTOs and commit the transaction, what happens in the database?
    Nothing. The DTO was never added to the persistence context, so there is no loaded-state snapshot and no dirty check at flush. To change data you must load the managed entity by its identifier and mutate that, or issue a bulk JPQL update statement.
  • How would you return a parent with its child collection as DTOs, given that a constructor expression cannot take a collection?
    Run two queries: one for the parent rows, then one that fetches the child rows for the collected parent ids, and stitch them in memory by parent id. That is two round trips and no duplicated parent rows. Hibernate 6 alternatively offers the `multiset()` HQL function, which builds nested lists inside a single statement.

saying these in an interview costs you the question

  • Believing DTO results are managed and that mutating them will be persisted
  • Writing the simple class name in the `new` clause instead of the fully qualified name
  • Calling it a projection while actually selecting whole entities and mapping them to DTOs in Java afterwards
  • Trying to pass a collection association as a constructor argument
  • Annotating the DTO as `@Entity` so it ends up mapped and managed

context

open as a page

What extra work and memory does Hibernate spend on a query that returns managed entity objects, compared with the same query returning a DTO projection?

level: middleimportance: must knowfreq 58%

basics

~20 s

For each managed entity Hibernate builds the instance, keeps it in the persistence context, and stores a second copy of its loaded state for dirty checking. Every flush then compares all of them property by property. A DTO row costs one small object and none of that.

open as a page

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%

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.

open as a page

You are asked to speed up a read-heavy list endpoint that currently loads mapped entities and maps them to a response in Java. Walk through converting it to a DTO read path and what you have to watch out for.

level: seniorimportance: should knowfreq 48%

basics

~20 s

Start from the response payload, write a query selecting exactly those columns into a DTO, push filtering, sorting, paging and aggregates into SQL, and drop the entity load. Watch for lost lazy navigation, formatting logic that lived on the entity, and per-row work now missing.

open as a page

How would you organise a service so that writes go through mapped JPA entities while reads are served by DTO projections — a lightweight read/write model split — and what does that approach cost you?

level: principalimportance: nice to knowfreq 36%

basics

~20 s

Keep one database and one schema. Commands load entities by id and mutate them; queries run projection queries into per-use-case DTOs and never return entities. Costs: two sets of types over one schema, duplicated knowledge of the mapping, and weaker reuse of entity behaviour and caching.

open as a page