How do you return a DTO from a custom @Query using a JPQL constructor expression, and what are its constraints?
answer
- select new FQN(...) from Entity
- Positional + type matching, not by name
- JPQL only — never native SQL
- Records work as the target class
- Closed by construction: only listed paths
basics
~10 sIn JPQL use select new com.example.MyDto(u.firstname, u.lastname) from User u. You must give the DTO's fully-qualified class name and pass constructor arguments in the exact order and types the constructor expects.
solid answer
~40 sWhen you write a custom @Query and want a DTO, JPQL's constructor expression is the standard mechanism: `@Query("select new com.example.UserNameDto(u.firstname, u.lastname) from User u")`. Key constraints: the class name must be **fully qualified** (unless using a JPA 3.2+ / Hibernate feature that relaxes this), the arguments are matched **positionally by type and order** to a real constructor, the DTO must have a matching public constructor, and it is a plain class (not necessarily an entity). Unlike derived-query DTO projections, name-based matching does not apply here — you control the SELECT list explicitly, so it is inherently a closed projection selecting only listed columns. It works with records too. You cannot use `select new ...` with native SQL queries; for native queries you need @SqlResultSetMapping or an interface projection.
code
java · 10 linespublic record DeptStat(String dept, long headcount) {}
public interface UserRepository extends JpaRepository<User, Long> {
@Query("""
select new com.example.dto.DeptStat(u.department, count(u))
from User u
group by u.department
""")
List<DeptStat> countByDepartment();
}go deeper
Know the select new FQN(...) syntax and that it returns a DTO.
Explain positional/type matching, FQN requirement, and that it is JPQL-only.
Contrast name-based (derived) vs positional (constructor expression) matching and the native-SQL alternative (@SqlResultSetMapping).
Advise on portability across Hibernate versions, aggregation read models, and when to prefer Blaze-Persistence/QueryDSL for complex projections.
**What a constructor expression is.** JPQL (the JPA query language) has a special `SELECT new` syntax that instantiates an arbitrary class per result row, passing the selected values to its constructor. Spring Data exposes this through `@Query`: ```java @Query("select new com.example.dto.UserNameDto(u.firstname, u.lastname) from User u where u.active = true") List<UserNameDto> findActiveNames(); ``` **Rules and constraints.** 1. **Fully-qualified class name** is traditionally required (`com.example.dto.UserNameDto`), because JPQL has no import mechanism. (Hibernate 6.2+/JPA 3.2 added support for records and, in some setups, short names via `@Imported`/instantiation improvements, but the portable, always-safe form is the FQN.) 2. **Positional, type-based matching.** The constructor arguments are bound **by position and type**, not by name. The listed expressions must line up with a constructor whose parameter types are assignable from the selected expression types, in the same order. 3. **A real matching constructor must exist.** If no constructor matches the argument list, you get a query/instantiation error at startup or execution. 4. **Closed by construction.** Because you explicitly list the projected paths, only those columns are selected — efficient by design. 5. **JPQL only, not native SQL.** `select new` is invalid in a `nativeQuery = true` @Query. For native queries, map results with `@SqlResultSetMapping` (+ `@ConstructorResult`) or use an interface projection whose getter names match the SQL column aliases. 6. **Aggregates work.** You can pass computed values: `select new com.example.Stat(u.dept, count(u)) from User u group by u.dept`. **Records.** A Java record's canonical constructor works perfectly as the target; modern Hibernate even supports omitting the class for record instantiation in newer versions, but FQN remains the safe default. **Constructor expression vs derived DTO projection.** With a *derived* query (no @Query) Spring Data auto-generates the constructor expression and matches **by name**; with a *manual* @Query you write the expression yourself and matching is **by position/type**. This is a common interview trap: name matching (derived) vs positional matching (constructor expression). **Common errors.** - Forgetting the package (`select new UserNameDto(...)`) → 'class not found' / parse error (unless your Hibernate version supports short names). - Argument type mismatch (e.g., selecting a `Long` count into an `int` param). - Trying `select new` in a native query. - Expecting only-needed columns from an *interface open projection* — those load the whole entity; the constructor expression does not. **When to use.** Whenever you need a custom query (joins, filters, aggregations) and a lightweight, immutable result — the constructor expression is the canonical JPA-portable choice.
- Can you use a JPQL constructor expression with nativeQuery = true?No. `select new` is JPQL-only. For native SQL, use @SqlResultSetMapping with @ConstructorResult, or an interface projection whose getters match the column aliases.
- In a constructor expression, are arguments matched to the DTO by name or by position?By position and type. This differs from derived-query DTO projections, which match constructor parameter names to entity property names.
- Why is the fully-qualified class name usually required?JPQL has no import statement, so historically the parser needs the full package path to resolve the class. Newer Hibernate versions relax this for records, but FQN is the portable, safe form.
saying these in an interview costs you the question
- Using select new in a native query
- Assuming name-based matching in constructor expressions
- Omitting the package and expecting it to resolve portably
- Thinking constructor expressions load the whole entity