💾 Section 16 · Question #29

What is Projection in JPA

A Projection in JPA and Spring Data is a mechanism for retrieving a strictly defined subset of entity fields or calculated expressions directly from the database rather than loa...

A Projection in JPA and Spring Data is a mechanism for retrieving a strictly defined subset of entity fields or calculated expressions directly from the database rather than loading entire managed entities. Utilizing projections is one of the most effective query optimization strategies: it minimizes network bandwidth between the database and the JVM, reduces heap memory allocation, and completely eliminates Hibernate’s overhead related to First-Level Cache management, entity snapshotting, and dirty checking.


🟢 Junior Level

Why Projections are Essential

When executing a standard query like SELECT u FROM User u, Hibernate:

  1. Generates a SELECT * query fetching all table columns (including wide text columns, metadata, and audit fields).
  2. Instantiates full User Java objects in heap memory.
  3. Clones the state into an internal loadedState array in the First-Level Cache (L1) for dirty checking.
  4. Initializes relation proxy handlers.

If a view only requires displaying a user’s name and email address in a summary table, loading full entities wastes substantial system resources. Projections fetch only the required columns:

// Immutable Java Record:
public record UserSummary(String name, String email) {}

// JPQL Constructor Expression:
TypedQuery<UserSummary> query = entityManager.createQuery("""
    SELECT new com.example.dto.UserSummary(u.name, u.email)
    FROM User u
    """, UserSummary.class);

List<UserSummary> summaries = query.getResultList();
// Generated SQL: SELECT name, email FROM users;
// Bypasses L1 snapshotting and dirty checking overhead!

🟡 Middle Level

5 Projection Varieties in JPA and Spring Data

1. Untyped Raw Array (Object[])

List<Object[]> rows = entityManager.createQuery("SELECT u.name, u.email FROM User u").getResultList();
String name = (String) rows.get(0)[0]; // Brittle and type-unsafe

2. Tuple Projection (jakarta.persistence.Tuple)

Provides column access via string aliases with type safety at extraction:

List<Tuple> tuples = entityManager.createQuery("""
    SELECT u.name AS userName, u.email AS userEmail FROM User u
    """, Tuple.class).getResultList();

String name = tuples.get(0).get("userName", String.class);

3. DTO Constructor Expressions (SELECT new ...)

Standard JPA mechanism supporting both standard Java classes and Java Records:

public record UserInfoDto(Long id, String email) {}

@Query("SELECT new com.example.dto.UserInfoDto(u.id, u.email) FROM User u")
List<UserInfoDto> fetchUsers();

Requirement: The fully qualified class name including package must be specified in the JPQL query string (new com.example.dto.UserInfoDto(...)).

4. Closed Interface Projections (Spring Data JPA)

Spring Data allows defining interface contracts with getter methods:

public interface UserContactView {
    String getName();
    String getEmail();
}

public interface UserRepository extends JpaRepository<User, Long> {
    List<UserContactView> findAllByActiveTrue();
}

Spring Data inspects the interface getters and generates optimized SQL querying only the name and email columns.

5. Dynamic Projections

A single repository method can return different projection representations dynamically based on a type token:

public interface UserRepository extends JpaRepository<User, Long> {
    <T> List<T> findByStatus(String status, Class<T> type);
}

// Usage:
List<UserSummary> list1 = repo.findByStatus("ACTIVE", UserSummary.class);
List<UserDetailDto> list2 = repo.findByStatus("ACTIVE", UserDetailDto.class);

🔴 Senior Level

The Open Projection Trap in Spring Data (@Value / SpEL)

Spring Data supports “Open Projections” using the Spring Expression Language (SpEL):

// ❌ HIDDEN PERFORMANCE DEGRADATION:
public interface UserFullNameView {
    @Value("#{target.firstName + ' ' + target.lastName}")
    String getFullName();
}

Why Open Projections are Harmful:

  • With a Closed Projection (String getFirstName()), Spring Data optimizes the SQL query: SELECT first_name FROM users.
  • With an Open Projection using @Value, Spring Data cannot reliably determine which fields will be referenced by the SpEL expression during runtime evaluation.
  • Consequence: Spring Data executes a full SELECT *, loading the entire database row into memory, wraps it inside a dynamic proxy, and evaluates the expression in the JVM. All database and network bandwidth optimization is lost.

Projections and Collections: JPA Specification Limitations

The JPA specification prohibits passing collections directly into constructor expressions:

// ❌ INVALID JPQL SYNTAX:
public record OrderWithItemsDto(Long orderId, List<ItemDto> items) {}

// Compiling this query throws a parsing error:
@Query("SELECT new com.example.OrderWithItemsDto(o.id, o.items) FROM Order o")
List<OrderWithItemsDto> findOrders();

Senior Strategies for Projecting 1:N Hierarchies:

  1. Two-Query Staged Retrieval: Retrieve the parent DTOs first, then fetch the child DTOs via an IN (:parentIds) query, grouping children into their parents in application memory using Collectors.groupingBy().
  2. Hibernate TupleTransformer: In Hibernate 6, transform flat SQL JOIN rows into a nested DTO structure using a custom TupleTransformer.
  3. Blaze-Persistence: An enterprise extension on top of JPA that provides native support for nested collection projections directly in queries.

4 Tricky Questions

1. Are DTOs or projections retrieved via SELECT new UserDto(...) managed by Hibernate’s Dirty Checking mechanism?

Answer:

  1. No, absolutely not.
  2. DTO objects are not JPA entities (@Entity). They are instantiated directly via standard Java constructors (new UserDto(...)) populated from the JDBC ResultSet.
  3. They are not stored in the StatefulPersistenceContext.entitiesByKey map and have no corresponding EntityEntry.
  4. Mutating properties on a DTO will never trigger an SQL UPDATE statement upon transaction commit.

2. Why are Java Records preferred over Spring Data Interface Projections in high-throughput (HighLoad) systems?

Answer:

  1. For interface projections, Spring Data creates a dynamic JDK proxy (java.lang.reflect.Proxy) wrapping an internal attribute map for every single row in the result set.
  2. Proxy generation and reflective getter invocation introduce measurable CPU overhead and heap allocation pressure when processing large result sets (10,000+ rows).
  3. Java Records compile to native bytecode constructors. They are instantiated directly without reflection, providing minimal heap footprint and zero proxy indirection.

3. Can you combine Spring Data pagination (Pageable) with an interface or record projection?

Answer:

  1. Yes, fully supported.
  2. A repository method such as Page<UserView> findByActive(boolean active, Pageable pageable) executes two optimized queries:
    • SELECT COUNT(id) FROM users WHERE active = ? (calculating the total count for pagination).
    • SELECT id, name, email FROM users WHERE active = ? LIMIT ? OFFSET ? (fetching only the projected columns for the requested page slice).

4. What happens if a DTO constructor parameter type does not match the JPQL query expression type (e.g., Long in constructor vs. Integer from the database)?

Answer:

  1. Hibernate uses reflection to look up a constructor whose parameter types match the query expression types exactly.
  2. The JPA query compiler does not perform automatic widening conversions (e.g., auto-casting Integer to Long or Double to BigDecimal).
  3. If no matching constructor is located, query bootstrap fails at runtime with: org.hibernate.query.SemanticException: Unable to locate appropriate constructor on class [com.example.UserDto].

🎯 Interview Cheat Sheet

30-Second Elevator Pitch

“JPA Projections allow fetching only required columns rather than full managed entities. This reduces network payload size, cuts JVM heap memory allocation, and eliminates the overhead of First-Level Cache snapshotting and dirty checking. Core patterns include DTO Constructor Expressions (using Java Records SELECT new ...) and Spring Data closed interface projections. A critical architectural caveat: open interface projections using @Value SpEL expressions force Spring Data to fetch all columns (SELECT *), defeating the optimization. For read-only queries and REST API responses, projecting directly into Java Records is the industry gold standard.”


Key Production Metrics & Indicators

  • GC Allocation Rate (MB/sec): Replacing entity queries with DTO projections typically reduces per-request memory allocation by 60–80%.
  • Network Ingress/Egress on Database Links: Significant reduction in transferred bytes when excluding wide text/binary columns.
  • HikariCP Connection Hold Time: Queries return faster because less tabular data is streamed and serialized across the network.

Red Flags (What NOT to Say)

  • ❌ “Modifying a DTO projection updates the database when the transaction commits.” — Projections are plain unmanaged objects; dirty checking does not track them.
  • ❌ “Open projections with @Value are the most performant way to compute fields.” — False; open projections cause Hibernate to fetch all entity columns.
  • ❌ “You can pass child @OneToMany collections directly into JPQL constructor expressions.” — Forbidden by the JPA specification; causes query parsing errors.
  • ❌ “Always load full entities for read-only reports and API endpoints.” — Anti-pattern causing excessive memory consumption and potential $N+1$ query issues.