Careeroza
Projections and DTO Queries
Why Not Load Whole Entities?
A product card on the home page needs id, name and price. Loading full entities fetches every column, puts every object into the persistence context and keeps snapshots for dirty checking. A projection selects only the needed columns and returns read-only objects. Faster, lighter, and no lazy-loading surprises.
Interface Projection
public interface ProductCard {
Long getId();
String getName();
BigDecimal getPrice();
@Value("#{target.price.compareTo(T(java.math.BigDecimal).valueOf(500)) <= 0}") // open projection (computed)
boolean isUnder500();
}
public interface ProductRepository extends JpaRepository<Product, Long> {
List<ProductCard> findTop12ByStockGreaterThanOrderByIdDesc(int stock);
}-- closed interface projections select only the needed columns
-- (open projections with @Value load the whole entity, so use them rarely)
select p1_0.id,p1_0.name,p1_0.price from products p1_0 where p1_0.stock>? order by p1_0.id desc limit ?DTO (Record) Projection with a Constructor Expression
package com.shopease.order.dto;
public record OrderSummary(Long orderId, String customerEmail, BigDecimal total, long itemCount) {}
public interface OrderRepository extends JpaRepository<Order, Long> {
@Query("""
select new com.shopease.order.dto.OrderSummary(o.id, u.email, o.total, count(i))
from Order o join o.user u left join o.items i
where o.status = :status
group by o.id, u.email, o.total
order by o.id desc
""")
List<OrderSummary> summaries(@Param("status") OrderStatus status);
}select o1_0.id,u1_0.email,o1_0.total,count(i1_0.id)
from orders o1_0
join users u1_0 on u1_0.id=o1_0.user_id
left join order_items i1_0 on o1_0.id=i1_0.order_id
where o1_0.status=?
group by o1_0.id,u1_0.email,o1_0.total
order by o1_0.id descTip: select new needs the fully qualified class name and a constructor whose parameter types match the selected values. Records are perfect DTOs: immutable and with a canonical constructor.
Dynamic Projections
One repository method, different result shapes chosen by the caller:
<T> List<T> findByCategoryName(String categoryName, Class<T> type);
List<ProductCard> cards = productRepository.findByCategoryName("Books", ProductCard.class);
List<Product> full = productRepository.findByCategoryName("Books", Product.class);| Projection | Selects | Use for |
|---|---|---|
| Entity | all columns, managed | when you will modify the data |
| Closed interface | only getters' columns | quick read-only views from derived queries |
| Record / class (DTO) | exactly what you select | API responses, reports, aggregates |
Tuple / Object[] | anything | rarely; untyped |