Spring Data Projections — Practical Demo
Hands-on examples for Spring Data Projections. All examples use the
Product/Customerdomain to show how projections reduce over-fetching and prevent sensitive field leakage.
Ensure you understand Spring Data Repositories and JPA Basics. See Spring Data Projections for the full theory.
Example 1: Closed Interface Projection
Load only id, name, and price — skip the heavy description and thumbnail columns entirely.
@Entity
public class Product {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private BigDecimal price;
private String description; // ← large text column
private byte[] thumbnailImage; // ← binary column — expensive
}
public interface ProductSummary { // ← interface — Spring generates a proxy implementing this
Long getId();
String getName();
BigDecimal getPrice();
// ← description and thumbnailImage are NOT declared here → NOT fetched from DB
}
public interface ProductRepository extends JpaRepository<Product, Long> {
List<ProductSummary> findAllProjectedBy(); // ← Spring infers projection from return type
Optional<ProductSummary> findProjectedById(Long id);
}
SQL generated (only declared columns):
SELECT p.id, p.name, p.price FROM products p
-- thumbnail_image NOT loaded — saves IO on large binary columns
@GetMapping("/products")
public List<ProductSummary> listProducts() {
return productRepo.findAllProjectedBy(); // ← safe: no thumbnailImage in response
}
The interface getter names (getId, getName, getPrice) match entity field names exactly. Spring Data derives column selection from these names at startup time.
Example 2: Nested Projection for Associations
Include selected fields from a related entity in a single query — eliminates N+1.
public interface OrderSummary {
Long getId();
String getStatus();
CustomerInfo getCustomer(); // ← nested projection — one level deep
interface CustomerInfo { // ← inner interface for Customer fields
String getName();
String getEmail();
// ← customer.passwordHash is NOT included → not fetched → not exposed
}
}
public interface OrderRepository extends JpaRepository<Order, Long> {
List<OrderSummary> findByStatus(String status);
}
SQL generated (single JOIN — no N+1):
SELECT o.id, o.status, c.name, c.email
FROM orders o
INNER JOIN customers c ON o.customer_id = c.id
WHERE o.status = ?
List<OrderSummary> pending = orderRepo.findByStatus("PENDING");
pending.forEach(o ->
System.out.println(o.getCustomer().getName() + " — " + o.getStatus())
);
// → "Alice — PENDING"
// → "Bob — PENDING"
// All loaded in ONE query. Customer password never exposed.
Example 3: DTO Projection with Java Records
Use a record for an immutable, serialization-friendly DTO loaded via a JPQL constructor expression.
public record ProductPriceInfo(Long id, String name, BigDecimal price) {}
// ↑ constructor parameter order must match SELECT order below
public interface ProductRepository extends JpaRepository<Product, Long> {
@Query("SELECT new com.example.dto.ProductPriceInfo(p.id, p.name, p.price) FROM Product p")
List<ProductPriceInfo> findAllPriceInfo();
@Query("SELECT new com.example.dto.ProductPriceInfo(p.id, p.name, p.price)" +
" FROM Product p WHERE p.price < :maxPrice ORDER BY p.price ASC")
List<ProductPriceInfo> findAffordable(@Param("maxPrice") BigDecimal maxPrice);
}
List<ProductPriceInfo> affordable = productRepo.findAffordable(new BigDecimal("50.00"));
affordable.forEach(p ->
System.out.printf("%-30s $%.2f%n", p.name(), p.price())
);
// → "USB Hub $19.99"
// → "Screen Cleaner $9.99"
SQL generated:
SELECT p.id, p.name, p.price FROM products p WHERE p.price < ? ORDER BY p.price ASC
-- No description or thumbnail loaded
DTO projections with records are not managed by the Hibernate session — they are plain Java objects. No dirty checking, no session overhead, fully immutable.
Example 4: Dynamic Projections
One repository method returns different shapes depending on the caller.
public interface ProductRepository extends JpaRepository<Product, Long> {
<T> List<T> findByCategory(String category, Class<T> type); // ← generic
}
// Consumer A — needs only summary
List<ProductSummary> summaries =
productRepo.findByCategory("electronics", ProductSummary.class);
// Consumer B — needs full entity (e.g., for admin panel)
List<Product> full =
productRepo.findByCategory("electronics", Product.class);
// Summary query → SELECT id, name, price WHERE category = ?
// Full query → SELECT * WHERE category = ?
Exercises
- Easy: Add a
getCategory()getter toProductSummaryand observe that the SQL now includescategoryin theSELECTclause. - Medium: Create a
CustomerListViewprojection interface withgetId(),getName(), and a nestedAddressViewprojection forgetAddress()that includesgetCity()andgetCountry(). Verify thatSELECTcontains only those columns. - Hard: Write a
@DataJpaTestintegration test that: (a) saves aProductwith a largedescription, (b) callsfindAllProjectedBy(), (c) asserts the result has the rightnameandprice, and (d) uses a SQL logging interceptor to assertdescriptiondoes NOT appear in the generatedSELECTstatement.
Back to Topic
Return to Spring Data Projections for theory, projection type comparison table, interview questions, and further reading.