JPA hides SQL, and the queries it generates by default are the most common Spring performance problem.
The N+1 problem: you load N orders with one query, then touching order.getCustomer() for each triggers N more queries. It's invisible in code and painful in production. Make it visible first: enable spring.jpa.show-sql or, better, log org.hibernate.SQL at DEBUG and count queries in tests.
Fetching rules:
@ManyToOneand@OneToOneare eager by default in JPA; mark themfetch = FetchType.LAZY. Collections (@OneToMany) are lazy.- Load what a use case needs in one query with a fetch join,
select o from Order o join fetch o.customer, or@EntityGraph(attributePaths = "customer")on a repository method. hibernate.default_batch_fetch_size(e.g. 50) batches lazy loads intoINqueries as a safety net.
Spring Boot enables open-in-view by default, keeping the persistence context open during view rendering so lazy loading works in controllers; it logs a warning at startup. Set spring.jpa.open-in-view=false so lazy loading outside transactions fails loudly instead of firing hidden queries.
For read-only screens, query projections instead of entities: an interface or record with just the needed columns (List<BookSummary> findByAuthor(String a)), avoiding dirty-checking overhead. Page large results with Pageable (findAll(PageRequest.of(0, 20, Sort.by("title")))); fetch-joining collections while paging forces in-memory pagination, so avoid that combination.
Example
@Entity
class Order {
@Id @GeneratedValue Long id;
@ManyToOne(fetch = FetchType.LAZY) Customer customer; // default would be EAGER
BigDecimal total;
}
record OrderSummary(Long id, String customerName, BigDecimal total) {}
interface OrderRepository extends JpaRepository<Order, Long> {
// 1 query instead of 1 + N
@EntityGraph(attributePaths = "customer")
List<Order> findByTotalGreaterThan(BigDecimal min);
// DTO projection: only the needed columns, no managed entities
@Query("select new com.example.OrderSummary(o.id, c.name, o.total) " +
"from Order o join o.customer c where o.total > :min")
Page<OrderSummary> summaries(@Param("min") BigDecimal min, Pageable page);
}
// application.properties
// spring.jpa.open-in-view=false
// spring.jpa.properties.hibernate.default_batch_fetch_size=50
// logging.level.org.hibernate.SQL=DEBUGRemember
- N+1: one query for the list plus one per element; count queries to catch it
- Make to-one associations LAZY and fetch per use case with join fetch or @EntityGraph
- Disable open-in-view so hidden lazy loading fails fast
- Use DTO projections and paging for read-heavy endpoints
Watch out for
- Leaving @ManyToOne at its eager default, so every load of the entity pulls related rows you didn't need.