By WebNest Studio Engineering Team
Spring Boot Tutorial
Hibernate and JPA Entity Mapping
Hibernate is the JPA (Jakarta Persistence API) implementation that Spring Boot uses by default under Spring Data JPA. JPA defines the standard annotations and interfaces; Hibernate is the engine that actually translates your annotated Java classes into SQL and manages the object-relational mapping at runtime.
Core Entity Annotations
A class becomes a persistent entity through a small set of annotations that describe how it maps to a database table and columns.
@Entity— marks a class as a JPA-managed entity, mapped to a database table.@Id— marks the primary key field;@GeneratedValueconfigures how it is generated (IDENTITY, SEQUENCE, AUTO).@Column— customizes a field's mapped column name, length, nullability, or uniqueness (optional if defaults are acceptable).@Table— customizes the mapped table name when it should differ from the class name.
Default Names and Reserved Words
Without @Table and @Column, Spring Boot derives names from your Java code: OrderLine becomes the table order_line and createdAt becomes the column created_at. That is convenient until a class name is also an SQL keyword. An entity called Order maps to a table called order, and ORDER is reserved in every SQL database. On H2 every DDL statement for it fails with Syntax error in SQL statement "create table [*]order ...". Hibernate only logs these failures as warnings, so the application starts, but the table was never created. Give such entities a safe name with @Table(name = "orders"); the same applies to User, Group and Select.
Generated Schema Details
With spring.jpa.show-sql=true you can read the DDL Hibernate generates. A few details often surprise people. A BigDecimal without @Column(precision, scale) becomes numeric(38,2) in Hibernate 6 and 7 (older tutorials show numeric(19,2) from Hibernate 5), so set the precision you actually need, such as @Column(precision = 10, scale = 2) for money. Columns are not created in field order. And for an embedded database like H2, Spring Boot defaults spring.jpa.hibernate.ddl-auto to create-drop: tables are created at startup and dropped at shutdown. For a real database the default is none, and the schema should come from Flyway or Liquibase migrations rather than from Hibernate.
Mapping Relationships
JPA models real-world relationships between entities using dedicated annotations: @OneToMany/@ManyToOne for parent-child relationships (e.g. one Customer has many Orders), @ManyToMany for relationships needing a join table (e.g. Students and Courses), and @OneToOne for a strict one-to-one link. The "owning side" of a relationship is the one holding the foreign key (here Order.customer). The inverse side is marked with mappedBy, which tells Hibernate "this list is described by the customer field on the other class". Leave mappedBy out and Hibernate treats the list as a second, unrelated relationship: it creates an extra join table customer_orders and writes every link twice.
Keeping Both Sides in Sync
In a bidirectional relationship Hibernate only reads the owning side when it writes SQL. If you add an order to customer.getOrders() but never call order.setCustomer(customer), the customer_id column stays empty. The usual fix is a pair of helper methods on the parent, addOrder() and removeOrder(), that update both sides at once, and to use only those methods from the rest of the code.
Cascade and orphanRemoval
cascade = CascadeType.ALL passes operations from the parent to its children: saving a customer saves its new orders, and deleting the customer deletes its orders first. orphanRemoval = true goes one step further: an order removed from the list is deleted from the database, even though the customer still exists. Use both only when the children really belong to the parent (order lines in an order, addresses of a customer). Never cascade from a child to a shared parent, for example cascade = ALL on Order.customer, because deleting one order would then delete the customer and all their other orders.
Fetch Types and Lazy Loading
Every association has a fetch strategy — FetchType.LAZY (load on first access) or FetchType.EAGER (load immediately with the parent). @ManyToOne and @OneToOne default to EAGER; @OneToMany and @ManyToMany default to LAZY. Lazy associations accessed outside an active persistence context throw a LazyInitializationException, which is one of the most common runtime errors in JPA-based applications. Set fetch = FetchType.LAZY on every @ManyToOne and decide per query what to load.
The N+1 Query Problem
Lazy loading has a cost that is easy to miss. Load 3 customers and then read each customer's orders, and Hibernate runs 1 query for the customers plus 1 more per customer: 4 queries. With 500 customers on a page that becomes 501 round trips to the database. The fix is to load what the screen needs in one query, either with a JOIN FETCH in JPQL (select distinct c from Customer c left join fetch c.orders) or with @EntityGraph(attributePaths = "orders") on a Spring Data repository method. Turning on show-sql while you develop is the simplest way to notice the problem early.
Examples
A one-to-many relationship between Customer and Order (tested on Spring Boot 4.1.1 / Hibernate 7 / H2)
@Entity
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Order> orders = new ArrayList<>();
protected Customer() {} // required by JPA
public Customer(String name) { this.name = name; }
public void addOrder(Order order) { orders.add(order); order.setCustomer(this); }
public void removeOrder(Order order) { orders.remove(order); order.setCustomer(null); }
// getters omitted
}
@Entity
@Table(name = "orders") // "order" is an SQL keyword
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private BigDecimal total;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id")
private Customer customer;
protected Order() {}
public Order(BigDecimal total) { this.total = total; }
void setCustomer(Customer customer) { this.customer = customer; }
// getters omitted
}
# application.properties
spring.jpa.show-sql=true
Hibernate: create table customer (id bigint generated by default as identity, name varchar(255) not null, primary key (id))
Hibernate: create table orders (total numeric(38,2), customer_id bigint, id bigint generated by default as identity, primary key (id))
Hibernate: alter table if exists orders add constraint FK624gtjin3po807j3vix093tlf foreign key (customer_id) references customer
Saving a customer with two orders (cascade):
Hibernate: insert into customer (name,id) values (?,default)
Hibernate: insert into orders (customer_id,total,id) values (?,?,default)
Hibernate: insert into orders (customer_id,total,id) values (?,?,default)
LazyInitializationException, N+1 queries and the JOIN FETCH fix
public interface CustomerRepository extends JpaRepository<Customer, Long> {
@Query("select distinct c from Customer c left join fetch c.orders")
List<Customer> findAllWithOrders();
}
// 1. Outside a transaction: the session is already closed
Customer first = customers.findAll().get(0);
first.getOrders().size();
// 2. Inside a transaction: works, but one extra query per customer
tx.executeWithoutResult(s -> customers.findAll()
.forEach(c -> System.out.println(c.getName() + " has " + c.getOrders().size() + " orders")));
// 3. One query for everything
customers.findAllWithOrders()
.forEach(c -> System.out.println(c.getName() + " has " + c.getOrders().size() + " orders"));
1.
Hibernate: select c1_0.id,c1_0.name from customer c1_0
LazyInitializationException: Cannot lazily initialize collection of role 'demo.Customer.orders' with key '1' (no session)
2.
Hibernate: select c1_0.id,c1_0.name from customer c1_0
Hibernate: select o1_0.customer_id,o1_0.id,o1_0.total from orders o1_0 where o1_0.customer_id=?
Asha has 2 orders
Hibernate: select o1_0.customer_id,o1_0.id,o1_0.total from orders o1_0 where o1_0.customer_id=?
Ravi has 2 orders
Hibernate: select o1_0.customer_id,o1_0.id,o1_0.total from orders o1_0 where o1_0.customer_id=?
Meera has 2 orders
3.
Hibernate: select distinct c1_0.id,c1_0.name,o1_0.customer_id,o1_0.id,o1_0.total from customer c1_0 left join orders o1_0 on c1_0.id=o1_0.customer_id
Asha has 2 orders
Ravi has 2 orders
Meera has 2 orders
orphanRemoval and cascading delete
// Remove one order from Asha's list (inside a transaction)
tx.executeWithoutResult(s -> {
Customer asha = customers.findById(1L).orElseThrow();
asha.removeOrder(asha.getOrders().get(0));
});
// Delete Ravi entirely
customers.deleteById(2L);
Hibernate: select c1_0.id,c1_0.name from customer c1_0 where c1_0.id=?
Hibernate: select o1_0.customer_id,o1_0.id,o1_0.total from orders o1_0 where o1_0.customer_id=?
Hibernate: delete from orders where id=? <- orphanRemoval
Hibernate: select c1_0.id,c1_0.name from customer c1_0 where c1_0.id=?
Hibernate: select o1_0.customer_id,o1_0.id,o1_0.total from orders o1_0 where o1_0.customer_id=?
Hibernate: delete from orders where id=? <- cascade REMOVE
Hibernate: delete from orders where id=?
Hibernate: delete from customer where id=?
What goes wrong without mappedBy
// Customer, with mappedBy removed:
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private List<Order> orders = new ArrayList<>();
Hibernate: create table customer_orders (customer_id bigint not null, orders_id bigint not null unique)
...
Hibernate: insert into customer (name,id) values (?,default)
Hibernate: insert into orders (customer_id,total,id) values (?,?,default)
Hibernate: insert into orders (customer_id,total,id) values (?,?,default)
Hibernate: insert into customer_orders (customer_id,orders_id) values (?,?)
Hibernate: insert into customer_orders (customer_id,orders_id) values (?,?)
(The link is now stored twice: in orders.customer_id and in the extra
customer_orders table.)
Common Mistakes
- Accessing a LAZY collection or association after the transaction/session has already closed, causing a LazyInitializationException.
- Forgetting mappedBy on the inverse side of a bidirectional relationship, which causes Hibernate to create an unwanted extra join table.
- Using FetchType.EAGER everywhere "to be safe," which quietly loads far more data than needed and hurts performance at scale.
- Not setting cascade carefully on @OneToMany, leading to either orphaned child rows or unintended deletions when the parent changes.
- Naming an entity Order, User or Group without @Table. The DDL fails on the reserved word, Hibernate only logs a warning, and the missing table shows up later as query errors.
- Updating only one side of a bidirectional relationship, so the foreign key column is never written.
- Leaving @ManyToOne at its EAGER default, which adds a join or an extra query every time the child is loaded.
- Relying on ddl-auto=update in production instead of versioned migrations with Flyway or Liquibase.
Exercise
A teammate is about to make this mistake: Accessing a LAZY collection or association after the transaction/session has already closed, causing a LazyInitializationException. Use Core Entity Annotations to review the decision. Write a corrected implementation or design sketch that follows Hibernate is the default JPA implementation used by Spring Data JPA in Spring Boot., then name one edge case you would check.
Show hints
- Start with Core Entity Annotations and compare your approach with A one-to-many relationship between Customer and Order (tested on Spring Boot 4.1.1 / Hibernate 7 / H2). Identify the decision or behavior the lesson recommends.
- Walk through your scenario step by step. Confirm that it follows Hibernate is the default JPA implementation used by Spring Data JPA in Spring Boot. and that it would catch the failure described by Accessing a LAZY collection or association after the transaction/session has already closed, causing a LazyInitializationException..
Quick quiz
1. Which choice best describes the recommended approach in Hibernate and JPA Entity Mapping?
- A. Accessing a LAZY collection or association after the transaction/session has already closed, causing a LazyInitializationException.
- B. Forgetting mappedBy on the inverse side of a bidirectional relationship, which causes Hibernate to create an unwanted extra join table.
- C. Hibernate is the default JPA implementation used by Spring Data JPA in Spring Boot.
Show answer
C. This is the recommended approach: Hibernate is the default JPA implementation used by Spring Data JPA in Spring Boot. Avoid: Accessing a LAZY collection or association after the transaction/session has already closed, causing a LazyInitializationException.
Interview questions
Why does this principle matter in Hibernate and JPA Entity Mapping: Hibernate is the default JPA implementation used by Spring Data JPA in Spring Boot.
Show answer
Hibernate is the default JPA implementation used by Spring Data JPA in Spring Boot. Apply this in Core Entity Annotations; the example A one-to-many relationship between Customer and Order (tested on Spring Boot 4.1.1 / Hibernate 7 / H2) shows one way to do it. Avoid Accessing a LAZY collection or association after the transaction/session has already closed, causing a LazyInitializationException. and explain how your implementation or design prevents that failure.
Key Points to Remember
- Hibernate is the default JPA implementation used by Spring Data JPA in Spring Boot.
- @Entity, @Id, @GeneratedValue, and @Column define how a class maps to a table; use @Table for names that clash with SQL keywords.
- @OneToMany, @ManyToOne, @ManyToMany, and @OneToOne map relationships between entities, with mappedBy identifying the inverse side.
- Helper methods such as addOrder()/removeOrder() keep both sides of a bidirectional relationship consistent.
- cascade passes save and delete from parent to children; orphanRemoval deletes children removed from the collection.
- LAZY vs EAGER fetch strategy controls when related data loads, and mismanaging it is a very common source of bugs.
- Watch for N+1 queries with show-sql and fix them with JOIN FETCH or @EntityGraph.
Practice in the playground
Write and run your own code for this topic, or start from the exercise or an example. Runs plain Java 17. Frameworks such as Spring, and databases, are not available here. Your code for each topic is kept in this browser.
Run the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
These examples need a JDK project with their libraries on your computer, so they can be copied but not run here. Codelab runs Python, FastAPI backends with SQLite, plain Java 17 and HTML/CSS/JavaScript.
Written by
WebNest Studio Engineering Team
Practical lessons from developers working with Java, Python, React, databases and production systems.
Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering