What is Projection in JPA
Projection is loading only the needed fields of an entity instead of the entire object. This significantly improves performance, reduces memory usage, and eliminates the need fo...
Overview
Projection is loading only the needed fields of an entity instead of the entire object. This significantly improves performance, reduces memory usage, and eliminates the need for dirty checking in read-only operations.
Junior Level
What is Projection
Projection - loading a subset of fields instead of the entire entity.
// Full entity - all fields
SELECT u FROM User u
// Projection - only needed fields
SELECT u.name, u.email FROM User u
Why Use It
- Less data from DB -> faster query
- Less data in memory -> savings
- No dirty checking -> less overhead
- Controlled API contract
Simple Example
// DTO class
public record UserDto(String name, String email) {}
// JPQL with constructor
@Query("SELECT new com.example.UserDto(u.name, u.email) FROM User u")
List<UserDto> findUserDtos();
Middle Level
Types of Projection
1. Array Projection
@Query("SELECT u.name, u.email FROM User u")
List<Object[]> findNamesAndEmails();
// Usage
List<Object[]> results = repository.findNamesAndEmails();
for (Object[] row : results) {
String name = (String) row[0];
String email = (String) row[1];
}
Drawbacks: type-unsafe, inconvenient
2. DTO Projection (Constructor Expression)
public record UserDto(String name, String email) {}
@Query("SELECT new com.example.UserDto(u.name, u.email) FROM User u")
List<UserDto> findUserDtos();
Advantages: type-safe, convenient
3. Interface-Based (Spring Data)
interface UserNameOnly {
String getName();
String getEmail();
}
List<UserNameOnly> findBy();
Advantages: Spring creates a proxy via the interface and maps columns by getter name. getName() -> column “name”. If name doesn’t match - null.
4. Class-Based (Spring Data)
public class UserDto {
private String name;
private String email;
public UserDto(String name, String email) {
this.name = name;
this.email = email;
}
}
List<UserDto> findBy();
Examples
// Aggregation
@Query("SELECT new com.example.UserStats(u.name, COUNT(o), SUM(o.total)) " +
"FROM User u LEFT JOIN u.orders o GROUP BY u.id")
List<UserStats> findUserStats();
// With condition
@Query("SELECT new com.example.OrderSummary(o.id, o.status, u.name) " +
"FROM Order o JOIN o.user u WHERE o.createdAt > :date")
List<OrderSummary> findRecentOrders(@Param("date") LocalDate date);
Common Mistakes
// Without full class name
@Query("SELECT new UserDto(u.name, u.email) FROM User u") // wrong
// With full name
@Query("SELECT new com.example.UserDto(u.name, u.email) FROM User u") // correct
// Wrong argument order
public record UserDto(String email, String name) {} // email, name
@Query("SELECT new com.example.UserDto(u.name, u.email) FROM User u") // wrong - name, email
// Correct - order must match
public record UserDto(String name, String email) {}
@Query("SELECT new com.example.UserDto(u.name, u.email) FROM User u") // correct
Senior Level
Performance Comparison
Full entity:
- SELECT * -> loads all fields
- Dirty checking overhead
- L1 cache storage
- O(N) memory per entity
Projection:
- SELECT specific columns
- No dirty checking
- Less memory
- O(1) memory per DTO
Difference:
Projection significantly saves memory (only needed columns loaded) and speeds up query (less data over network). Exact numbers depend on entity size, DB, and network.
Advanced Patterns
// Pattern 1: Nested DTO
public record OrderDto(
Long id,
String status,
UserDto user,
List<ItemDto> items
) {}
@Query("""
SELECT new com.example.OrderDto(
o.id, o.status,
new com.example.UserDto(u.name, u.email),
// items can be loaded via second query with @EntityGraph or
// separate query: SELECT i FROM Item i WHERE i.orderId IN (:orderIds)
null
)
FROM Order o JOIN o.user u
WHERE o.id = :id
""")
OrderDto findByIdWithUser(@Param("id") Long id);
// Pattern 2: Tuple-based projection
@Query("SELECT u.name as name, u.email as email FROM User u")
List<Tuple> findUsers();
List<UserDto> dtos = tuples.stream()
.map(t -> new UserDto(t.get("name", String.class),
t.get("email", String.class)))
.toList();
// Pattern 3: Dynamic projection with Criteria API
public <T> List<T> findUsers(Class<T> type) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<T> query = cb.createQuery(type);
Root<User> root = query.from(User.class);
if (type == UserDto.class) {
query.select(cb.construct(UserDto.class,
root.get("name"), root.get("email")));
} else {
query.select(root);
}
return em.createQuery(query).getResultList();
}
When to Use Projection
Projection for:
- Read-only operations
- API responses
- Lists/tables
- Aggregation and statistics
- Large data sets
Full entity for:
- CRUD operations
- When all fields needed
- When dirty checking needed
- Small data sets
Best Practices
DTO projection for read-only
Interface-based for flexibility
Class-based (record) for type-safety
Full class name in JPQL
Correct argument order
Array projection (type-unsafe)
Full entity for read-only
Without full class name
Wrong constructor argument order
Interview Cheat Sheet
Must know:
- Projection - loading only needed fields instead of entire entity
- 4 types: Array (type-unsafe), DTO constructor (type-safe), Interface-based (Spring), Class-based (Spring)
- Advantages: less data from DB, no dirty checking overhead, controlled API contract
- DTO projection requires full class name: new com.example.UserDto(…)
- Constructor argument order must match SELECT order
- For read-only/API - projection, for CRUD - full entities
Frequent follow-up questions:
- DTO vs Interface-based projection? DTO (record) - type-safe, explicit constructor; Interface - Spring creates proxy, getter mapping by name
- Why is Array projection bad? Type-unsafe: row[0], row[1] - errors on refactoring, inconvenient
- Performance difference? Projection saves memory (only needed columns) and speeds up query (less data over network)
- Can nested DTO be done? Yes - new com.example.OrderDto(new UserDto(u.name), …) in JPQL
Red flags (DO NOT say):
- “Array projection for production code” - type-unsafe, row[0] easily breaks
- “Without full class name in JPQL” - ClassNotFoundException
- “Full entity for read-only API” - unnecessary dirty checking, more memory
- “Wrong constructor argument order” - data mixed up, name -> email
Related topics:
- [[26. What is JPQL and How Does It Differ from SQL]]
- [[27. What is Criteria API and When to Use It]]
- [[25. How to Avoid Infinite Recursion When Serializing Entities]]
- [[4. What is LazyInitializationException and How to Avoid It]]