Stage 5 — Spring Data JPA: Entities, Tables, SQL, and Transactions
Spring Data JPA is used to work with a database through Java objects. Instead of manually writing insert into, select, and update in every method and converting rows by hand, the developer describes a Java class as an entity, maps it to a table, and uses a repository. SQL still exists, but Hibernate generates much of the common SQL.
A beginner should not start with Persistence Context and LazyInitializationException. First understand the simpler chain: a database table stores rows, a Java Entity represents one row of that table, JPA annotations describe the connection between class and table, Hibernate reads that connection and executes SQL, and Spring Data JPA provides a convenient repository API.
What an Entity is and how it maps to SQL
An Entity is a Java class that corresponds to a database table. One entity object usually corresponds to one table row. Entity fields correspond to table columns. This connection is called mapping: we explain to Hibernate which class is stored in which table and which field is connected to which column.
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "status", nullable = false)
private String status;
@Column(name = "total_amount", nullable = false)
private BigDecimal totalAmount;
}
@Entity says that this class is a JPA entity. @Table(name = "orders") says that objects of this class are stored in the orders table. @Id marks the primary key. @GeneratedValue says that the id value is created by the database or provider. @Column defines the column name and simple constraints.
This class corresponds roughly to a SQL table like this:
create table orders (
id bigserial primary key,
status varchar(255) not null,
total_amount numeric(19, 2) not null
);
If you call orderRepository.save(order), you do not write insert manually. Hibernate looks at the mapping and prepares SQL. For a new entity, this is an insert; for a changed managed entity, it is an update; for lookup by id, it is a select. SQL does not disappear. It is generated from Java classes and JPA annotations.
JPA, Hibernate, and Spring Data: who does what
JPA is a specification, meaning a set of rules and annotations for ORM in Java. ORM means Object-Relational Mapping: a connection between the Java object model and the relational database model. The word “contract” here means an API agreement: which annotations exist, what @Entity means, how EntityManager works, and which states an entity can have. JPA itself is not the concrete library that executes SQL in your application.
Hibernate is an implementation of JPA. It reads JPA annotations, stores mapping information, creates SQL, sends queries to the database through JDBC, receives result rows, and converts them back into Java objects. When people say “JPA saved the entity” in a Spring Boot project, Hibernate often did the actual work.
Spring Data JPA is a convenience layer on top of JPA/Hibernate. It provides repository interfaces, such as JpaRepository<Order, Long>, so you do not write the same code for findById, save, delete, pagination, and simple queries.
public interface OrderRepository extends JpaRepository<Order, Long> {
List<Order> findByStatus(String status);
}
The findByStatus method has no body, but Spring Data understands the method name and builds a query. For the orders table, it is logically similar to select * from orders where status = ?. For complex cases you can write JPQL or native SQL explicitly, but common repository features are usually the starting point.
| Layer | Simple explanation | Example |
|---|---|---|
| JPA | Rules and annotations for connecting objects to tables | @Entity, @Table, @Id |
| Hibernate | Library that implements those rules and executes SQL | Generates select, insert, update |
| Spring Data JPA | Convenient repository interfaces on top of JPA | JpaRepository, findByStatus |
| PostgreSQL/MySQL | Real database where tables and rows live | Table orders |
How to use a repository in a service
A controller should not directly decide how to save an order. Usually the controller receives the HTTP request, the service performs the business scenario, and the repository works with the database. This keeps responsibilities clear.
@Service
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public Order createOrder(BigDecimal totalAmount) {
Order order = new Order();
order.setStatus("NEW");
order.setTotalAmount(totalAmount);
return orderRepository.save(order);
}
}
When save is called, Spring Data JPA delegates the work to the JPA provider, usually Hibernate. Hibernate understands that the object is new and prepares insert into orders (...) values (...). If SQL logging is enabled, you can see the real queries sent to the database. This is useful because the repository hides boilerplate code, but it should not hide understanding.
What @Transactional means in simple words
A transaction is the boundary of one complete database operation. If the operation succeeds, changes are saved with commit. If an error occurs, changes are reverted with rollback. This prevents the database from staying in a half-updated state.
Imagine a money transfer: one account must be debited and another account must be credited. If the debit succeeds but the credit fails, the data is broken. A transaction makes those steps one operation: either all steps succeed, or all steps are rolled back.
In Spring, @Transactional is usually placed on a service method because the service describes the full business operation. This annotation is not decorative: Spring creates a transaction boundary around the method. Before entering the method, a transaction is opened; inside the method, repositories do database work; if the method succeeds, commit happens; if a runtime exception occurs, rollback usually happens.
@Transactional
public void payOrder(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
order.setStatus("PAID");
}
There is no explicit orderRepository.save(order) after the status change. If Order was loaded inside the transaction, Hibernate tracks it. On commit, Hibernate sees that status changed and executes update orders set status = ? where id = ?. This is called dirty checking.
Entity lifecycle and Persistence Context
Now Persistence Context can be explained. Persistence Context is Hibernate's working area inside a transaction where it stores loaded entity objects and tracks their changes. While an entity is in this area, it is called managed. Managed means Hibernate knows this object and can synchronize it with the database.
If an object was just created with new Order() but not saved yet, it is transient. If it was saved or loaded inside a transaction, it is managed. If the transaction ended and the object is no longer connected to the Hibernate session, it is detached. A detached object is still a normal Java object, but Hibernate no longer tracks its changes automatically.
Flush is the moment when Hibernate synchronizes changes from the Persistence Context to the database through SQL. Usually flush happens before commit or before some queries. Therefore SQL may appear not exactly at the setStatus line, but later when Hibernate decides to send changes.
Lazy vs Eager
Relations between entities can be loaded immediately or later. For example, an order may have a customer:
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id")
private Customer customer;
LAZY means: do not load Customer immediately with Order; load it when order.getCustomer() is accessed. EAGER means: try to load the related entity immediately. At first, EAGER looks simpler, but in real applications it often causes extra data loading and heavy queries. Many relations are safer as lazy by default, with explicit loading for the screen or API that needs the data.
LazyInitializationException happens when code tries to access a lazy relation after the transaction has ended and the Persistence Context is closed. Hibernate wants to load data, but its working area is gone. That is why data for a response should usually be loaded inside the service method and converted to a DTO there.
N+1: why it happens and how to diagnose it
N+1 happens when an application first loads a list of main entities with one query, then loads a related entity or collection separately for each item. For example, one query loads 100 orders, then 100 additional queries load customers for those orders. The result is 101 queries.
In a small local database this may be invisible. In production it quickly becomes slow. To see the problem, enable SQL logging and count the real queries. Solutions depend on the use case: fetch join, @EntityGraph, a separate DTO query, or a manual JPQL/native query.
Practical example
You need to create an order payment endpoint. The controller receives POST /orders/{id}/pay, the service opens a transaction through @Transactional, the repository loads the order, and the service changes status to PAID. Hibernate keeps the order as a managed entity. At the end of the method, the transaction commits, Hibernate performs dirty checking, and generates an update.
If the same response must include the customer name, the service should load the customer inside the transaction or use a DTO query immediately. If you simply return the entity and later access order.getCustomer().getName() after the transaction, you may get LazyInitializationException.
Stage takeaway
Spring Data JPA is not magic and not a replacement for SQL thinking. An Entity describes how a Java class maps to a table. JPA defines the rules, Hibernate executes SQL by those rules, and Spring Data JPA provides convenient repository interfaces. @Transactional defines the operation boundary where Hibernate can track managed entities and synchronize changes with the database automatically.
Understanding checklist
- I can explain what
@Entity,@Table,@Id, and@Columndo in mapping. - I understand the difference between JPA, Hibernate, and Spring Data JPA.
- I understand that SQL is generated by Hibernate from mapping and repository calls.
- I can explain why service methods are annotated with
@Transactional. - I understand that a managed entity is tracked by Hibernate inside Persistence Context.
- I can explain
LazyInitializationExceptionand N+1 with a simple example.