RizTech Academy logo
RizTech Academy
JPA and Hibernate in DepthLesson 3 of 530 min

Projections: fetching only what you need

Loading a whole entity when you only need two of its fields is wasteful — every column fetched, every relationship considered, the object tracked by the persistence context. Projections let you fetch exactly the fields you need, as a lighter result. For read-heavy endpoints (a list view, a dropdown, a report) projections are a real, measurable optimisation. This lesson covers the projection types, verified against a running Dakiya.

Why not always load the whole entity?

Loading a full entity has costs that add up on read-heavy paths:

  • Every column is selected, including large text or fields you will not use.
  • The entity is managed — the persistence context tracks it for dirty checking, using memory, even though you are only reading.
  • Relationships may trigger extra work (lazy proxies, or eager loads).

For an endpoint that returns a list of parcels showing only the tracking code and destination — a common case — loading full Parcel entities fetches far more than the response needs. A projection fetches only the columns the response uses, from a simpler query. The rule: for read-only endpoints that use a few fields, project; load the whole entity when you will modify it (so dirty checking works).

Interface projections: the simplest

Declare an interface with getters for exactly the fields you want, and a repository method returning it:

public interface ParcelSummary {
    String getTrackingCode();
    String getDestinationCity();
}
public interface ParcelRepository extends JpaRepository<Parcel, Long> {
    List<ParcelSummary> findSummaryByDestinationCity(String city);   // returns projections, not entities
}

Spring Data sees the return type is a projection interface and selects only the mapped columns (tracking_code, destination_city) — not the whole row. Verified against the running app: findSummaryByDestinationCity("Pune") returned 3 ParcelSummary objects, each exposing only getTrackingCode() and getDestinationCity() — there is no getRecipientName() on the projection, because that column was not fetched. The projection is a lightweight, read-only view of just the fields you named.

Interface projections also support nested projections (a getter returning another projection interface, for a related entity's fields) and derived closed vs open projections — but the flat, closed interface above covers the common case.

Class (DTO) projections

You can also project into a concrete class — often a record — via its constructor, with a JPQL constructor expression:

public record ParcelSummaryDto(String trackingCode, String destinationCity) {}
@Query("select new com.riztech.dakiya.ParcelSummaryDto(p.trackingCode, p.destinationCity) from Parcel p")
List<ParcelSummaryDto> summaries();

The select new <FQN>(...) constructs the record directly from the selected columns — again fetching only those columns. Class projections give you a concrete, immutable object (a record) you control, which is handy when the projection is also your API response DTO. Interface projections need less code (no query, no class body); class/record projections give you a real object with methods if you need them. Both fetch only the named columns; pick by whether you want the zero-boilerplate interface or a concrete record.

Projections and the API layer

Projections and DTOs (the REST module) work together: a projection is how you fetch a subset from the database; a DTO is what you expose at the API. Often they are the same shape, and a record projection can serve as both — the query fetches exactly the response fields, and you return it directly. Other times you project to a lean read model and still map to a separate API DTO. Either way, the point is the same as DTOs': do not ship whole entities, and do not fetch more than you need. A list endpoint backed by a projection selects a few columns and returns them — fast, lean, and with no lazy-loading surprises (a projection is not a managed entity with lazy proxies, so it sidesteps LazyInitializationException too).

When projections are worth it — and when not

Be proportionate:

  • Worth it: read-heavy list endpoints, dropdowns, reports, and any query returning many rows where you use a few fields. Fewer columns and no entity tracking is a genuine saving at scale.
  • Not worth it: when you need the whole entity anyway (you will modify it, or you use most fields). Projecting then would just add a type for no gain, and you cannot modify a projection (it is read-only — no dirty checking).
  • The measure: as with all performance work, do not project speculatively everywhere. Use whole entities by default; reach for a projection where a read-heavy query fetches far more than the response uses. The performance lesson closes this module with how to see that.

The habit to carry: fetch the narrowest shape the caller needs — a projection for read-only subsets, a full entity when you will change it. It is the query-level counterpart to returning DTOs at the API: both refuse to move more data than necessary.

Check your work

Why project. A full entity fetches every column, is tracked by the persistence context, and may trigger relationship work; a projection fetches only the fields you name — lighter, for read-only subsets.

Interface projections. An interface of getters for the wanted fields, returned by a repository method; Spring Data selects only those columns. Verified: the summary projection exposed only trackingCode + destinationCity (no recipientName — that column was not fetched).

Class/record projections. JPQL select new <FQN>(...) builds a record from selected columns — a concrete, immutable object you control. Interface = less code; record = a real object (and can double as the API DTO).

With the API. Projection = how you fetch a subset; DTO = what you expose; often the same shape, and a record can serve both. Projections also sidestep lazy-loading surprises (not managed entities).

When. Read-heavy list/report queries using a few fields → project; whole entity when you will modify it or use most fields; do not project speculatively — measure.

Practice

  1. Add a ParcelSummary interface projection and a findSummaryBy... method; confirm it returns only the projected fields (reproduce the verified result) and that show-sql selects only those columns.
  2. Add a record projection with a select new ... JPQL query; confirm it also fetches only those columns.
  3. Compare the SQL for a full-entity query versus the projection for the same rows — count the columns selected.
  4. Try to modify a projection and confirm you cannot persist a change through it (read-only, no dirty checking).
  5. Use a record projection as both the query result and the API response DTO for a list endpoint.
  6. For three Dakiya read endpoints, decide whole entity vs projection and justify each.

Official documentation

Next: dynamic queries with Specifications and Criteria.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship