How it works
The explanation, with examples you can run.
Repositories
@Entityclass Order { @Id @GeneratedValue Long id; @ManyToOne(fetch = FetchType.LAZY) Customer customer; @OneToMany(mappedBy = "order") List<OrderItem> items = new ArrayList<>(); Instant placedAt;} interface OrderRepository extends JpaRepository<Order, Long> { List<Order> findByCustomerIdAndPlacedAtAfter(Long customerId, Instant since); @Query("select o from Order o join fetch o.items where o.customer.id = :id") List<Order> findWithItems(@Param("id") Long customerId); @EntityGraph(attributePaths = {"items", "customer"}) List<Order> findTop20ByOrderByPlacedAtDesc();}Derived queries are parsed from the method name; a typo is a start-up error, not a runtime one. The methods inherited from JpaRepository (save, findById, ...) are transactional, read-only for reads; query methods you declare yourself get no transaction settings unless you annotate them or the interface. Either way, your service should own the transaction that spans several calls.
Lazy loading
JPA's defaults: @OneToMany and @ManyToMany are lazy; @ManyToOne and @OneToOne are eager unless you say fetch = FetchType.LAZY, which is the safer habit. A lazy association is a proxy or a wrapper collection. Touching it runs a SELECT through the persistence context, which must still be open, or you get a LazyInitializationException.
Spring Boot enables open session in view by default (spring.jpa.open-in-view=true) and logs a warning about it at start-up. It keeps the EntityManager open until the response is written, so lazy loading from the controller or the JSON serialiser "just works". The cost is that a database connection can be held for the whole request and queries happen far from the code that should own them. Many teams turn it off and fetch deliberately.
N+1
// 1 query for the orders...List<Order> orders = repo.findByCustomerIdAndPlacedAtAfter(id, since);// ...plus 1 per order, the first time items is touchedorders.forEach(o -> o.getItems().size());Fixes, chosen per use case:
- Fetch join in a
@Query(join fetch o.items) loads parents and children in one statement. Combined with pagination on a collection, Hibernate paginates in memory and warns, so page the parent ids first. @EntityGraphnames the associations to load for one repository method.- Batch fetching (
spring.jpa.properties.hibernate.default_batch_fetch_size=50) turns N extra queries into N/50. - DTO projections (
select new com.example.OrderRow(o.id, o.placedAt) ...or an interface projection) skip entities entirely for read-only lists.
Seeing the SQL
spring.jpa.show-sql=truelogging.level.org.hibernate.SQL=debuglogging.level.org.hibernate.orm.jdbc.bind=tracespring.jpa.properties.hibernate.generate_statistics=trueTurn these on in a test or locally, call the endpoint once and count the statements. A repeated select ... from order_item where order_id=? is N+1, every time.
Follow the flow
Play it or step through. Pause and tap a box to see what it does.
Paused · tap a box to explore it
The service method is @Transactional, so a persistence context, Hibernate's session, is open for as long as it runs. Every entity loaded inside it is tracked there.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.