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 desc

Tip: 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);
ProjectionSelectsUse for
Entityall columns, managedwhen you will modify the data
Closed interfaceonly getters' columnsquick read-only views from derived queries
Record / class (DTO)exactly what you selectAPI responses, reports, aggregates
Tuple / Object[]anythingrarely; untyped