JPA, Repositories, Transactions
The data layer โ where most production bugs live. Focus on the parts that trip experienced devs: N+1, transaction propagation, lazy loading.
JPA Mental Model
JPA is a specification, Hibernate is the implementation Spring Boot uses by default. The persistence context is the first-level cache โ a unit of work that tracks managed entities.
Entity Mapping
@Entity,@Table,@Id,@GeneratedValue,@Column@GeneratedValuestrategies:IDENTITY(DB auto-increment, most common) vsSEQUENCE
| Relationship | Default Fetch | Action |
|---|---|---|
@OneToMany | LAZY | Safe |
@ManyToMany | LAZY | Safe |
@ManyToOne | EAGER | Override to LAZY |
@OneToOne | EAGER | Override to LAZY |
@ManyToOne(fetch = FetchType.LAZY) and @OneToOne(fetch = FetchType.LAZY).
The eager defaults are the #1 cause of surprise N+1 queries.
Repositories
Repository โ CrudRepository โ JpaRepository โ each adds more methods.
save(),findById(),findAll(),delete(),count(),existsById()flush(),saveAndFlush()โ force writes to DB immediately- Pagination:
findAll(Pageable)returnsPage<T>with total count and page metadata
@Repository annotation is optional when extending JpaRepository โ Spring detects it automatically.
Query Styles
| Style | Example | When to Use |
|---|---|---|
| Derived method name | findByEmailAndStatus(...) | Simple conditions, 1-2 fields |
@Query JPQL | @Query("SELECT u FROM User u WHERE...") | Complex conditions, joins |
@Query native SQL | @Query(value="SELECT...", nativeQuery=true) | DB-specific features |
| Specification | repo.findAll(spec, pageable) | Dynamic filters (search APIs) |
JPQL uses entity class names and field names, not table/column names. Specifications are worth the complexity for filter-heavy search endpoints where the WHERE clause is dynamic.
Transactions
@Transactional โ begins tx before method, commits after, rolls back on unchecked exception.
Default: unchecked exceptions (RuntimeException) roll back, checked exceptions do not.
@Transactional(readOnly = true) โ use on all read methods.
No dirty checking, potential read-replica routing, and Hibernate flushes are skipped.
| Propagation | Behaviour |
|---|---|
REQUIRED (default) | Join existing tx, or create new one |
REQUIRES_NEW | Always create new tx, suspend existing |
MANDATORY | Must run inside existing tx โ throws if none |
SUPPORTS | Join if exists, run non-transactionally if not |
NOT_SUPPORTED | Always run non-transactionally, suspend existing |
NEVER | Must NOT run in a tx โ throws if one exists |
NESTED | Savepoint inside existing tx โ rollback to savepoint |
@Transactional method calling another
@Transactional method in the same class doesn't start a new transaction.
Extract to a separate bean.
The N+1 Problem
Loading 100 orders fires 1 query, then 100 more queries (one per order) to load items. LAZY loading triggers a new query each time you access the collection in a loop.
spring.jpa.show-sql=true + count the queries. Or use p6spy / slow query logs in production.
| Fix | How | Trade-off |
|---|---|---|
| JOIN FETCH in JPQL | SELECT o FROM Order o JOIN FETCH o.items | One query, may produce duplicates โ use DISTINCT |
@EntityGraph | @EntityGraph(attributePaths = {"items"} ) on repo method | Declarative, no JPQL needed |
@BatchSize | @BatchSize(size = 50) on collection | Reduces queries to N/50, not 1 |