Spring Data & JPA
Repositories, entities, JPQL, native queries, transactions, N+1 problem
Spring Data JPA abstracts the data access layer with Repository interfaces that generate SQL automatically. Understanding how JPA manages entities, when queries are executed, and how to avoid the N+1 problem is essential for building database-efficient Spring applications.
Key Points
- JpaRepository<Entity, Id>: extends CrudRepository + PagingAndSortingRepository — provides findAll, findById, save, delete and page/sort variants
- Derived query methods: findByEmailAndActiveTrue(), findByAgeBetween(int min, int max) — Spring generates JPQL from method name
- @Query: write JPQL or native SQL explicitly — use for complex queries that derived methods can't express
- @Transactional: service-layer annotation — demarcates transaction boundaries; @Transactional(readOnly=true) for read operations (performance hint to Hibernate)
- Entity relationships: @OneToMany, @ManyToOne, @ManyToMany — FetchType.LAZY is the default for collections, EAGER for single associations
- N+1 Problem: loading a list of entities then accessing a lazy collection on each one fires N extra queries — fetch eagerly with JOIN FETCH or @EntityGraph
- @EntityGraph: specify which associations to eagerly load per query — avoids changing the entity's default fetch type
- Projections: interface or record projections for SELECT subsets — avoids loading full entities when only a few fields are needed
- Optimistic locking: @Version field — JPA adds WHERE version = ? to UPDATE and throws OptimisticLockException on conflict
| Approach | Best for | N+1 risk |
|---|---|---|
| FetchType.LAZY | Most relationships — load on access | High if accessed in loop |
| JOIN FETCH (JPQL) | Known access patterns, single query | Solved — fetched in one query |
| @EntityGraph | Per-query fetch strategy without changing entity | Solved for that query |
| Projections | Read-only subsets, reports | Low — only selected columns |
| FetchType.EAGER | Rarely — loads always, even when not needed | Transfers to cartesian product risk |
Spring Data JPA: derived methods, JOIN FETCH for N+1, projections, @EntityGraph, @Version optimistic locking
@Entity
@Table(name = "orders")
public class Order {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id")
private Customer customer;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> items = new ArrayList<>();
@Version
private Long version; // optimistic locking
}
// Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
// Derived method
List<Order> findByCustomer_EmailAndStatus(String email, OrderStatus status);
// JPQL with JOIN FETCH — solves N+1 for items
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.customer.id = :customerId")
List<Order> findWithItemsByCustomer(@Param("customerId") Long id);
// Projection — only id + total, no full entity load
@Query("SELECT o.id AS id, SUM(i.price) AS total FROM Order o JOIN o.items i " +
"WHERE o.createdAt > :since GROUP BY o.id")
List<OrderSummary> findSummariesSince(@Param("since") LocalDate since);
// EntityGraph — fetch items eagerly for this query only
@EntityGraph(attributePaths = {"items", "items.product"})
Optional<Order> findWithItemsById(Long id);
}
// Service — transaction boundary
@Service @Transactional
public class OrderService {
@Transactional(readOnly = true) // tells Hibernate: no dirty checking
public List<OrderSummary> getRecentSummaries() {
return repo.findSummariesSince(LocalDate.now().minusDays(30));
}
}Real-World Example
The N+1 query problem is the #1 performance issue in Spring Data JPA apps. Diagnosis: enable SQL logging (spring.jpa.show-sql=true) and count queries for a list endpoint — if you see 1 + N queries for a list of N items, you have N+1. Fix: use JOIN FETCH in the query or @EntityGraph for the collection. Hibernate 6 (Spring Boot 3) improves batch loading with spring.jpa.properties.hibernate.default_batch_fetch_size=100.