Spring Data JPA and Hibernate Basics
Three layers are often confused. Interviewers like candidates who can separate them.
| Layer | What it is |
|---|---|
| JPA (Jakarta Persistence) | A specification: annotations (@Entity) and the EntityManager API |
| Hibernate | The most common JPA implementation: generates SQL, manages entity state |
| Spring Data JPA | A convenience layer that writes repository implementations for you, on top of JPA |
Definition
JPA maps Java objects to database tables, and Hibernate is the library that does the work. Objects you load inside a transaction are tracked, and their changes are written to the database as SQL when the transaction commits.
Why it exists
Writing JDBC by hand means converting rows to objects and back in every query. JPA does that mapping and change tracking for you.
How it works
The persistence context
Inside a transaction, Hibernate keeps a persistence context: a first-level cache of managed entities, keyed by id.
Entity states:
Consequences you should be able to explain:
- Identity guarantee: two
findById(1)calls in one transaction return the same Java object, with only one query. - Dirty checking: modify a managed entity and Hibernate issues the
UPDATEat flush/commit; you do not need to callsave. - Write-behind: SQL is deferred until flush (commit, before a query that may be affected, or an explicit
flush()). - Lazy loading only works while the context is open (see N+1 and lazy loading).
@Transactional
void markPaid(Long id) {
PurchaseOrder order = orders.findById(id).orElseThrow();
order.status = "PAID"; // no save() needed: dirty checking issues the UPDATE on commit
}
save, saveAll, deleteAll
savecallspersistfor new entities (id is null /@Versionnull) andmergefor existing ones.mergemay issue aSELECTfirst.deleteAll()loads and deletes entity by entity; usedeleteAllInBatch()or a@Modifying @Query("delete ...")for bulk deletes (these bypass the persistence context and entity callbacks).@Modifyingqueries should usually useclearAutomatically = true, otherwise managed entities show stale state.
Example code
A small domain
The examples across this section use customers, orders and order items.
@Entity
@Table(name = "purchase_orders", indexes = @Index(name = "idx_orders_status", columnList = "status"))
class PurchaseOrder {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "customer_id")
Customer customer;
@Column(nullable = false)
String status;
@Version
long version; // used for optimistic locking, see the Locking page
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
List<OrderItem> items = new ArrayList<>();
protected PurchaseOrder() { } // JPA needs a no-arg constructor
PurchaseOrder(Customer customer, String status) { this.customer = customer; this.status = status; }
void addItem(String sku, int quantity) {
items.add(new OrderItem(this, sku, quantity));
}
}
Details that matter:
@ManyToOneand@OneToOnedefault to EAGER; setLAZYexplicitly.@OneToManyand@ManyToManydefault to LAZY.mappedBymarks the inverse side. The side with the foreign key (OrderItem.order) owns the relationship, and only changes on the owning side are written to the database. Keep both sides in sync in a helper such asaddItem.cascade = ALLplusorphanRemoval = truemakes items part of the order's lifecycle.- Use
GenerationType.IDENTITYorSEQUENCE; avoidAUTOsurprises andTABLE.IDENTITYdisables JDBC insert batching;SEQUENCEallows it. - Avoid Lombok's
@Dataon entities (it generatesequals/hashCode/toStringthat touch lazy collections).
Repositories
Extend JpaRepository and Spring generates the implementation at startup:
interface PurchaseOrderRepository extends JpaRepository<PurchaseOrder, Long> {
List<PurchaseOrder> findByStatus(String status); // derived query
Page<PurchaseOrder> findByStatus(String status, Pageable pageable);
@Query("select distinct o from PurchaseOrder o join fetch o.items where o.status = :status")
List<PurchaseOrder> findWithItems(@Param("status") String status);
}
- Derived queries: the method name is parsed (
findByCustomerIdAndStatus,existsByEmail,countByStatus,deleteByStatus). Good for simple cases; long names are a sign to use@Query. @Querywith JPQL (entity names and fields) ornativeQuery = truefor SQL.- Prefer
@Queryor Specifications for anything complex; the Criteria API is verbose.
Pagination and sorting
Page<PurchaseOrder> page = orders.findByStatus("NEW", PageRequest.of(0, 20, Sort.by("id").descending()));
page.getContent(); // the 20 rows
page.getTotalElements(); // triggers an extra COUNT query
Page runs a count query; use Slice (no count) for infinite scroll. Never paginate a query that join fetches a collection: Hibernate would fetch everything and paginate in memory (Hibernate logs a "firstResult/maxResults specified with collection fetch; applying in memory" warning). Page the parent ids first, then fetch the children.
For deep pages, OFFSET gets slower as the offset grows; use keyset (seek) pagination (where id < :lastId order by id desc limit 20), see SQL tuning.
Projections
Fetching whole entities when you need two columns wastes memory and prevents index-only scans.
interface PurchaseOrderRepository extends JpaRepository<PurchaseOrder, Long> {
interface Summary { // interface-based projection
Long getId();
String getStatus();
}
List<Summary> findSummaryByStatus(String status);
}
record OrderSummaryDto(Long id, String status) { } // class/record-based (DTO) projection, via constructor expression
Projections return read-only data and are not managed entities.
System design considerations
- Log and review generated SQL in tests to catch N+1 queries
- Keep transactions short and avoid lazy loading in the web layer
- Use projections or DTO queries for read-heavy endpoints
When to use it, and when not
Use it when
- You model a domain with relationships and mostly standard CRUD
- You want transactions and change tracking handled for you
- You use Spring Data repositories for common queries
Think twice when
- You run heavy reporting or bulk updates where SQL or JDBC is clearer
- You need precise control over every statement and index usage
- You expose entities directly in APIs
Common mistakes
Open Session in View
Spring Boot enables spring.jpa.open-in-view=true by default: the persistence context stays open during the whole web request, so lazy loading works in controllers and view serialisation. It also holds a database connection for the whole request and hides N+1 queries. A common production setting is false, with services returning fully-loaded DTOs.
- Relying on EAGER mappings, which produce unexpected joins and extra queries everywhere.
- Updating only one side of a bidirectional relationship.
- Using
deleteAll()for large tables. - Returning entities directly from controllers (lazy proxies, over-exposure, infinite recursion in JSON); return DTOs.
- Importing
jakarta.persistence.*andorg.springframework.data.jpa.repository.*with wildcards:QueryandEntityGraphexist in both packages and the compiler reports "ambiguous reference".
Interview questions
1. JPA vs Hibernate vs Spring Data JPA?
JPA is the standard API, Hibernate implements it, and Spring Data JPA removes repository boilerplate by generating implementations on top of JPA.
2. What is the persistence context and what is dirty checking?
The persistence context is the first-level cache of managed entities in a transaction. On flush Hibernate compares each managed entity with its loaded snapshot and issues UPDATEs for the changes automatically.
3. save() vs saveAndFlush()?
save schedules the change; saveAndFlush also flushes immediately, forcing SQL (and constraint violations) to surface now.
4. getReferenceById vs findById?
findById hits the database and returns the entity or empty. getReferenceById returns a lazy proxy without querying, useful to set a foreign key (order.setCustomer(customers.getReferenceById(id))), and throws only when you touch the data if it does not exist.
5. What is the difference between persist and merge?
persist makes a new transient entity managed. merge copies the state of a detached (or new) instance onto a managed instance and returns that managed instance; the object you passed stays detached.
6. What do FetchType.LAZY and EAGER mean, and what are the defaults?
LAZY loads on first access; EAGER loads with the owner. To-one associations default to EAGER, to-many to LAZY; best practice is LAZY everywhere and fetching explicitly per use case.
7. Why avoid @Data and field-based equals/hashCode on entities?
They can trigger lazy loading, and an id-based hashCode changes after the entity is persisted, which breaks HashSets. Use a business key or a stable id strategy.
Check yourself
1You load an entity inside a transaction and change a field without calling save(). What happens at commit?
2What does the first-level cache guarantee?
3What is the main risk of Open Session in View?
Key takeaways
- JPA is the API, Hibernate the engine, Spring Data the productivity layer.
- Managed entities are dirty-checked automatically inside a transaction.
- Make associations LAZY and choose how to fetch per query.
- Use projections and DTOs for reads; page carefully.
Next: N+1 problem and lazy loading.
Next topicN+1 and lazy loadingFinished this page? Track your progress (saved only in your browser).