Spring Data JPA generates repository implementations for you at runtime. It's enormously productive, but a few defaults can bite you in production if you don't understand what's happening underneath.
Declaring a repository
Extend JpaRepository and you get CRUD operations for free. Derived query
methods are parsed from the method name.
public interface AuthorRepository extends JpaRepository<Author, Long> {
List<Author> findByLastName(String lastName);
Optional<Author> findByEmail(String email);
List<Author> findByActiveTrueOrderByCreatedAtDesc();
}The N+1 problem
The most common JPA performance trap: loading a list of entities, then lazily fetching an association for each one — turning one query into N+1.
// This triggers one query for authors,
// then one more per author for their books.
List<Author> authors = authorRepository.findAll();
authors.forEach(a -> a.getBooks().size()); // lazy load per authorFix it with a fetch join so the association loads in a single query.
@Query("SELECT a FROM Author a LEFT JOIN FETCH a.books")
List<Author> findAllWithBooks();Use projections for read models
When you only need a few columns, fetching full entities is wasteful. Interface projections let Spring build lightweight read models.
public interface AuthorSummary {
Long getId();
String getLastName();
}Rules of thumb
- Watch your SQL logs during development — enable
show-sql. - Reach for fetch joins or
@EntityGraphwhen you hit N+1. - Use projections for read-heavy endpoints.
JPA is powerful once you can see the queries it generates. Make them visible and the surprises disappear.