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
ApproachBest forN+1 risk
FetchType.LAZYMost relationships — load on accessHigh if accessed in loop
JOIN FETCH (JPQL)Known access patterns, single querySolved — fetched in one query
@EntityGraphPer-query fetch strategy without changing entitySolved for that query
ProjectionsRead-only subsets, reportsLow — only selected columns
FetchType.EAGERRarely — loads always, even when not neededTransfers 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.