Java Backend · Spring Data JPA

Spring Data JPA, from object to table

How a Spring Boot application talks to a relational database without you writing the SQL, the JDBC, or the mapping by hand, using the BuyBuddy e-commerce backend as the running example.

Session 4 · JAVA-301 · Updated August 2026 · Read ~18 min

Choose your route

Problem: an object model and relational tables disagree in useful, expensive ways. Budget 45 minutes to map a small model or 15 minutes to explain the trade-offs.

Learning goal

Recall first: sketch an object with an identifier and its table equivalent. Layered request flow is helpful; it is not required to open this page.

  • Map an entity.
  • Use a repository for CRUD.
  • Choose a relationship mapping deliberately.

Shared core: map meaning, not just fields

JPA maps entity state to tables; Spring Data generates repository operations over that model. The annotations describe identity, columns, and relationships, while transactions define a coherent persistence boundary.

Closest confusion: an entity is a managed object with persistence identity; a DTO is a transport shape. They may look alike but should not inherit each other’s responsibilities.

Failure trace: creating a fresh object for an update drops the managed row’s identity and may insert or overwrite unexpectedly. Load the row, change intended fields, then save inside the right boundary.

Decision rule: model relationships from ownership and access patterns, not from the hope that every Java association should be eager and bidirectional.

Version note: annotations and examples were reviewed for Spring Boot 4.x on 2026-08-28; verify provider and database-specific defaults during upgrades.

Learn deeply: persist an order and its customer

Mission (30 minutes): add Customer and PurchaseOrder entities with a many-to-one relationship, then expose one repository-backed create/read flow. Likely files: entities, repository, service, and H2 configuration.

Acceptance checks: schema starts cleanly; saving an order stores its customer key; reading the order returns the intended association without an accidental JSON loop.

  1. Start with identity and one scalar field per entity.
  2. Add the owning relationship and inspect generated SQL.
  3. Keep the controller response separate from the entity if the graph grows.
Solution shape, not a script

Let the many side hold the foreign key. Use a small response DTO once the endpoint needs a stable API contract.

Transfer: add a query for orders by customer and explain why query method names are not a substitute for understanding the SQL they imply.

Prepare for interviews: state the persistence trade-offs

Two-minute summary: JPA manages entity state and relationships; Spring Data removes repetitive repository plumbing but does not remove SQL, transactions, or loading decisions.

Explain in 60 seconds: why should API DTOs usually differ from entities?

Primary question: explain @OneToMany and @ManyToOne ownership. Follow-ups: what causes N+1 queries, and what does lazy loading change?

Design exercise: an endpoint serializes a customer with thousands of orders and times out. Choose a projection, pagination, or fetch strategy and defend the trade-off.

Strong-answer outline

Start with relational ownership, name the foreign key side, separate persistence from API shapes, and admit that fetch strategy follows a query, not a universal annotation preference.

Timed mock: design a safe order-update flow in 90 seconds.

Exit check

  1. Which side usually owns a many-to-one foreign key?
  2. What separates an entity from a DTO?
  3. Why load before an update?
  4. What creates an N+1 query?
  5. When does a repository method need a transaction boundary?

Learn next: Use map an entity as the base, then persist data with jpa in Real data with JPA, H2 & Lombok. Interview next: Use map an entity as the base, then distinguish test doubles in Test Doubles & Mockito.

Continue this path · Switch paths from this topic · Review first: layered flow

// the big idea

Spring Data JPA lets you treat a database like a collection of Java objects. You annotate a plain class, declare an empty repository interface, and the framework generates the SQL, the mapping, and the boilerplate for you, so you write intent (save, findById) instead of plumbing.

§ 1 — Notebook Summary

Notebook Summary

Copy this into a notebook. Everything here is unpacked in the Detailed Notes below.

The layer stack (top → bottom)

Mental model

Entity = class ↔ table

Relationships

Type Annotation Example
1:1 @OneToOne Person ↔ Passport
1:N @OneToMany / @ManyToOne Customer → Orders
M:N @ManyToMany (+ join table) Order ↔ Product

Repository

CRUD map

Op Method Returns
C save(e) saved entity
R findById(id) Optional<E>
R findAll() List<E>
U save(existing) updated entity
D deleteById(id) void

Setup essentials

§ 2 — Detailed Study Notes

Detailed Study Notes

You've been handed the backend for BuyBuddy, a small e-commerce app. It has to remember customers, products, and orders long after the server restarts, so all of it lives in a MySQL database. The instinct is to open an editor and start writing SQL: INSERT INTO customer…, then the code to open a connection, run the statement, read the result set row by row, close everything, map each column back into a Java field, and repeat that for every table and every operation. Do this for Customer, Product, and Order, times create/read/update/delete, and most of your codebase becomes plumbing that has nothing to do with e-commerce. Spring Data JPA exists to delete that plumbing.

💡 tip · start with clarity

The deck opens on a deliberate detour: before any code, it pins down the requirements (a customer places many orders; an order holds many products; a product appears in many orders). The quoted line is "the sooner you start to code, the longer the program will take." In an interview or a real project, restating the data relationships before touching a keyboard is the move that earns trust.

2.1 · The stack, who does what

The database access ecosystem is a stack of layers, each one thinner and more convenient than the one below it. The confusion this lesson exists to clear up is that "Spring Data JPA," "JPA," and "Hibernate" get used as if they mean the same thing. They don't; they sit at different heights in the same tower.

Analogy · restaurant order

Think of ordering food. JPA is the printed menu, a promise of what can be ordered but it cooks nothing. Hibernate is the chef who actually reads the menu and makes the dish. Spring Data JPA is the waiter who takes your one-line request ("the usual, saved") and handles the rest. JDBC is the kitchen doorway every plate physically passes through.

Spring Data JPA · removes boilerplate JPA · the spec (interface / contract) Hibernate · JPA provider, does the ORM ORM · objects ↔ tables JDBC · sends SQL, reads results Database · MySQL your call travels down
fig 2.1 — one request falls through six layers to reach MySQL
⭐ trick · interface vs class

The one sentence that untangles all of it: JPA is the interface, Hibernate is a class that implements it. You could swap Hibernate for another provider (EclipseLink, OpenJPA) and your JPA-annotated code wouldn't change, exactly like swapping one implementation of a Java interface for another.

⚠ pitfall · treating them as synonyms

A very common interview stumble is calling Hibernate "the same as JPA," or thinking Spring Data JPA replaces Hibernate. It doesn't: Spring Data JPA sits on top of a JPA provider and still relies on Hibernate to do the actual object-to-table work.

2.2 · Wiring MySQL to Spring Boot

Before any entities exist, the application needs to know which database to talk to and how to log in. That configuration lives in application.properties. Spring Boot reads it at startup and hands the connection details to Hibernate.

src/main/resources/application.properties
spring.application.name=buy-buddy-backend

spring.datasource.url=jdbc:mysql://localhost:3306/buybuddy_mysql_db
spring.datasource.username=root
spring.datasource.password=rootpassword

# How Hibernate handles DDL (table creation / alteration)
spring.jpa.hibernate.ddl-auto=update

The ddl-auto=update line is the one that feels magical: on startup, Hibernate inspects your entity classes and creates or alters the matching tables to fit. You get schema management for free, driven by your Java code.

⚠ pitfall · Hibernate won't make the database

Hibernate can create and alter tables, but it is not granted permission to create the database itself. You must do that once by hand before the first run, or startup fails with an "unknown database" error:

terminal — one-time setup
mysql -u root -p
CREATE DATABASE buybuddy_mysql_db;

Then run the app with ./gradlew bootrun. The distinction to hold onto: you create the empty database; Hibernate fills it with tables.

2.3 · Entities, turning a class into a table

An entity is an ordinary Java class that you've marked so JPA will store its objects as rows in a table. You describe the mapping with annotations, and the ORM does the translation both ways: your object becomes an INSERT, and a row you read back becomes an object.

Analogy · a labelled shipping box

An entity class is a box with a printed label. @Entity says "this box gets shipped," @Table writes the destination on it, @Id is the tracking number, and each @Column is a line on the packing slip. Hibernate is the warehouse worker who reads the labels and files the box in the right shelf.

The core annotations

Annotation Applied to Meaning
@Entity class This class maps to a table.
@Table(name=…) class Names the table (defaults to the class name).
@Id field Marks the primary key.
@GeneratedValue field Auto-generates the key; defaults to AUTO.
@Column(name=…) field Customizes the column mapping / name.
Customer.java
package com.example.buy_buddy_backend;

import javax.persistence.*;

@Entity
@Table(name = "customer")
public class Customer {

    @Id
    @GeneratedValue
    private Long id;

    @Column(name = "customer_name")
    private String name;

    private String email;   // no annotation -> column "email"
}

Notice email has no annotation and still becomes a column named email: annotations are for overriding defaults, not for switching mapping on. The one you almost always want explicitly is @Id, because every table needs a primary key.

💡 tip · javax vs jakarta

The deck imports from javax.persistence. Since the Jakarta EE rename, newer Spring Boot 3.x projects use jakarta.persistence instead. Same annotations, different package. [+] If your @Entity "isn't recognised," a mismatched import package is the usual culprit.

2.4 · Mapping relationships

A relationship is a connection between two tables that says how their rows relate. Relationships are how you organize data without repeating it: instead of copying a customer's details onto every order, an order just points back to one customer.

There are three shapes, and picking the right one is the whole game:

@OneToOne

1 : 1

One row here ↔ exactly one row there. Person ↔ Passport.

@OneToMany / @ManyToOne

1 : N

One row relates to many. Customer → Orders.

@ManyToMany

M : N

Many ↔ many, needs a junction table. Students ↔ Courses.

Mapping BuyBuddy's requirements onto these shapes:

Customer id · name · email Order id · customerId order_product orderId · productId Product id · name · price 1 : N M : N
fig 2.4 — Customer→Order is 1:N; Order↔Product is M:N through a join table

The relationship annotations

Annotation Side it goes on
@OneToOne either entity
@OneToMany the "one" side (e.g. Customer holds many Orders)
@ManyToOne the "many" side (e.g. Order belongs to one Customer)
@ManyToMany both entities of an M:N pair

Two more you'll see in the entity code: @JoinColumn names the foreign-key column in a relationship, and on the many-to-many, a join annotation names the columns pointing into the junction table. The deck also notes Lombok annotations (like @Getter/@Setter) generate the getters and setters so the entity class stays short. [+]

⭐ trick · read the two sentences, not the diagram

You never have to guess the relationship type. Read the requirement as two sentences and check both directions: "one customer → many orders" and "one order → one customer" gives 1:N. "one order → many products" and "one product → many orders" — many on both sides — gives M:N, which is the only case that needs a junction table.

2.5 · Repositories, database access with no code

A repository is an interface that gives you CRUD operations for one entity, and Spring writes the implementation for you. This is the payoff of the whole framework: you declare what you want, never how.

Analogy · a vending machine you design by its buttons

You describe the buttons you want on the front of the machine (save, findByEmail). You never build the mechanism behind the glass. Spring Data JPA fabricates the whole machine at runtime from your button labels, matching each button to a real database operation.

You get a repository by writing an interface that extends JpaRepository, parameterized with the entity type and its ID type:

CustomerRepository.java
import org.springframework.data.jpa.repository.JpaRepository;

public interface CustomerRepository extends JpaRepository<Customer, Long> {

    // Custom query, no body needed.
    // Spring parses the method name into a query.
    Customer findByEmail(String email);
}

That's the entire file. No class, no SQL, no connection handling. By extending JpaRepository<Customer, Long> you inherit save, findById, findAll, delete, and more. The <Customer, Long> pair tells Spring which entity and which primary-key type it's working with.

Controller→ Service→ Repository→ Database

the request path through a Spring Boot app

⭐ trick · derived query methods

The method name is the query. findByEmail(String email) becomes SELECT * FROM customer WHERE email = ? because Spring parses the name: findBy + a field name. You can chain them, findByNameAndEmail(...), and never write SQL for the common cases.

💡 tip · why interfaces for services too

The deck's code demo puts a contract (interface) in front of each service class, e.g. CustomerService with a CustomerServiceImpl. The point is the same as the repository: callers depend on the interface, so you can swap the implementation without touching them. It's the standard "program to an interface" best practice.

2.6 · CRUD operations in practice

CRUD is the four things you ever do to stored data: Create, Read, Update, Delete. Spring Data JPA ships ready-made methods for all four, so a whole service method is often one line.

Letter Method What it does
C save(entity) Inserts a new row, returns the saved entity.
R findById(id) Fetches one row as Optional<E>.
R findAll() Fetches every row as a List<E>.
U save(existing) Updates a row that already has an id.
D deleteById(id) Removes the row with that id.
Create & Read
// Create (C)
User user = new User("John", "john.doe@example.com");
userRepository.save(user);

// Read (R)
Optional<User> found = userRepository.findById(1L);
List<User> allUsers = userRepository.findAll();
Update & Delete
// Update (U): load, mutate, save again
User user = userRepository.findById(1L).get();
user.setEmail("new.email@example.com");
userRepository.save(user);

// Delete (D)
userRepository.deleteById(1L);
⭐ trick · save() does both create and update

There is no separate update(). save() decides: if the entity has no id (or an id not yet in the table) it inserts; if it already carries a known id it updates that row. So the update recipe is always load → change fields → save again.

⚠ pitfall · findById returns an Optional

findById hands back an Optional<User>, not a User, because the row might not exist. Calling .get() on an empty Optional throws NoSuchElementException. The safe habit is .orElseThrow(...) or .orElse(null); reach for .get() only when you've truly guaranteed the row is there.

Tying it together, the deck's Postman demo posts a customer, two products, then an order referencing them by id ({"customerId": 1, "productIds": [1, 2]}), and the request travels Controller → Service → Repository → Database. That single POST exercises the M:N mapping, the repository layer, and a save in one shot.

§ 2.7 — Interview & Practice Q&A

2.7 · Interview & Practice Q&A

Are JPA, Hibernate, and Spring Data JPA the same thing?

No. JPA is a specification, an interface defining how ORM should work. Hibernate is a JPA provider, a concrete implementation of that spec that does the actual object-to-table mapping. Spring Data JPA sits on top of a JPA provider and removes repository boilerplate. They are three layers, not synonyms.

What does @GeneratedValue do if you don't pass a strategy?

It defaults to AUTO, letting the JPA provider pick a generation strategy appropriate to the database. It marks the primary key as auto-generated so you don't set the id yourself when creating a new entity.

How is BuyBuddy's Order-to-Product relationship modelled, and why?

As Many-to-Many, because an order can contain many products and a product can appear in many orders, many on both sides. That requires a junction (join) table holding pairs of orderId and productId, which is what @ManyToMany generates.

You extend JpaRepository but write no implementation. Who does?

Spring Data JPA generates the implementation at runtime. From the interface's type parameters and method names it produces a proxy backed by the JPA provider, so declaring the interface is enough to get working CRUD and derived query methods.

How do you update an existing record?

Load it (usually with findById), change the fields on the returned object, then call save on it again. Because the entity already carries a known id, save performs an update rather than an insert. There is no separate update method.

Why does findById return an Optional?

Because a row with that id may not exist. Optional forces the caller to handle the empty case explicitly instead of silently returning null. Blindly calling .get() on an empty Optional throws NoSuchElementException, so prefer .orElseThrow(...).

What is spring.jpa.hibernate.ddl-auto=update doing?

It tells Hibernate how to handle Data Definition Language on startup. With update, Hibernate inspects your entities and creates or alters tables to match them, without dropping existing data. It manages tables only; you still create the database itself by hand first.

Where does JDBC fit in this stack?

At the bottom. Hibernate uses JDBC to send the generated SQL to the database and read results back. Spring Data JPA and JPA are higher-level abstractions; JDBC is the layer that actually communicates with the database engine.

§ 2.8 — Key Takeaways

2.8 · Key Takeaways

spec vs provider @Entity = table @Id + @GeneratedValue 1:N vs M:N junction table extends JpaRepository derived queries save = create+update findById → Optional ddl-auto=update
§ 3 — Microexercises

Microexercises

You can recognise these annotations now. These drills prove you can write them. Attempt each before opening the walkthrough.

3.1 · Build the Order entity and its repository

// your task

Write a JPA Order entity for BuyBuddy and a repository for it.

Requirements:

Expected result: two files that compile with no method bodies, exposing save/findById/findAll/deleteById for free plus your custom finder.

Show the full walkthrough

Step 1 — mark the class as an entity and fix the table name. order is reserved in SQL, so map to orders with @Table. Add the auto-generated key.

Step 2 — model the two relationships from the requirement sentences. "An order belongs to one customer" is the many side of Customer→Orders, so @ManyToOne with a @JoinColumn naming the foreign key. "An order has many products, a product is in many orders" is @ManyToMany.

Step 3 — declare the repository. Extend JpaRepository<Order, Long>. The custom finder walks a relationship, so name it after the path: findByCustomerId.

Order.java
package com.example.buy_buddy_backend.model;

import javax.persistence.*;
import java.util.List;

@Entity
@Table(name = "orders")   // "order" is a reserved SQL word
public class Order {

    @Id
    @GeneratedValue
    private Long id;

    @ManyToOne
    @JoinColumn(name = "customer_id")
    private Customer customer;

    @ManyToMany
    @JoinTable(name = "order_product", joinColumns = @JoinColumn(name = "order_id"), inverseJoinColumns = @JoinColumn(name = "product_id"))
    private List<Product> products;
}
OrderRepository.java
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;

public interface OrderRepository extends JpaRepository<Order, Long> {

    // Derived query: SELECT * FROM orders WHERE customer_id = ?
    List<Order> findByCustomerId(Long customerId);
}

Why: the two relationship annotations are a direct translation of the two requirement sentences, and the empty repository proves the framework's core promise, you declared the buttons and Spring built the machine.

3.2 · Diagnose the broken update

// your task

This service method is supposed to change a customer's email. It compiles, but it can crash at runtime and it can also silently create a duplicate. Predict the exact failure, name the root cause, and fix it.

CustomerServiceImpl.java — buggy
public Customer updateEmail(Long id, String newEmail) {
    Customer c = customerRepository.findById(id).get();
    Customer updated = new Customer();
    updated.setEmail(newEmail);
    return customerRepository.save(updated);
}

Expected answer: which exception fires when the id is missing, and why even a present id produces the wrong row.

Show the full walkthrough

Root cause — two bugs. First, .get() is called on the Optional from findById with no empty check. Second, and worse, the method builds a brand-new Customer and saves that instead of the loaded one.

Predicted failure 1 (missing id): if no customer has that id, findById(id) returns an empty Optional and .get() throws NoSuchElementException.

Predicted failure 2 (id exists): even on the happy path, updated has no id, so save treats it as new and inserts a second customer with only an email set, name and everything else null. The original row is never touched. You wanted an update; you got an accidental create.

Step 1 — handle the empty Optional with orElseThrow instead of .get().

Step 2 — mutate the loaded entity so its existing id makes save perform an update.

CustomerServiceImpl.java — fixed
public Customer updateEmail(Long id, String newEmail) {
    Customer c = customerRepository.findById(id).orElseThrow(() -> new RuntimeException("Customer not found: " + id));
    c.setEmail(newEmail);          // mutate the loaded entity
    return customerRepository.save(c);  // has an id -> UPDATE
}

Why: save decides insert-vs-update purely from whether the entity carries a known id, so the update recipe is always load the real row → change it → save it. Building a fresh object throws that id away.

@Table for reserved words @ManyToOne = many side @JoinTable for M:N findByCustomerId orElseThrow not .get() mutate, don't re-new save needs the id to update

Built from the JAVA-301 Session 4 deck (44 slides). No separate voice transcript was provided, so scope follows the full deck. Items marked [+] are added context beyond the slides; none change the taught material.