JPA & Database
JPA stands for Java Persistence API. It is a specification that defines how Java objects should be mapped to database tables. Instead of writing raw SQL queries manually in your Java code, you work with plain Java objects and JPA handles the SQL for you automatically behind the scenes.
Think of it like Mongoose for MongoDB but for SQL databases. With Mongoose you define a schema and Mongoose handles the database operations. With JPA you define an Entity class and JPA handles the SQL.
Mongoose (MongoDB) JPA (MySQL)
Schema → Entity class
Model → Repository
.find() → findAll()
.findById() → findById()
.save() → save()
.deleteOne() → deleteById()
Hibernate
JPA is just a specification — a set of rules that says "this is how Java objects should be mapped to database tables." But a specification alone does nothing. Someone has to actually implement those rules. Hibernate is that implementation.
You write JPA annotations (@Entity, @Id etc.)
↓
Spring Data JPA — provides Repository and method generation
↓
Hibernate — takes your Entity, generates SQL, executes it
↓
MySQL Driver — sends SQL to the database
↓
MySQL Database
You never interact with Hibernate directly in Spring Boot. You just write JPA annotations and call Repository methods. Spring Boot uses Hibernate behind the scenes automatically. When you see SQL printed in your console after setting spring.jpa.show-sql=true — that SQL was generated by Hibernate.
Without JPA you would have to write raw JDBC code for every single database operation :
// Without JPA — manual SQL, messy and error prone
public User getUserById(int id) {
Connection conn = DriverManager.getConnection(url, username, password);
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
stmt.setInt(1, id);
ResultSet rs = stmt.executeQuery();
User user = new User();
user.setId(rs.getInt("id"));
user.setName(rs.getString("name"));
user.setEmail(rs.getString("email"));
conn.close();
return user;
}
With JPA the same thing becomes :
// With JPA — clean and simple
public User getUserById(int id) {
return userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("User not found"));
}
Setting Up JPA
Add these dependencies to pom.xml :
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
Configure your database in application.properties :
spring.datasource.url=jdbc:mysql://localhost:3306/mydb
spring.datasource.username=root
spring.datasource.password=yourpassword
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
Folder Structure for JPA
When working with JPA every feature in your app follows the exact same folder structure. You never put database related code in the controller and you never put HTTP related code in the repository. Everything has its place. The 4 files you create for every feature :
model/
└── User.java ← defines what a User/entity looks like and maps to DB table
repository/
└── UserRepository.java ← all methods that talk to the DB for User
service/
└── UserService.java ← all business logic for User, uses Repository
controller/
└── UserController.java ← all HTTP endpoints for User, uses Service
NOTE : The model/ folder is sometimes also called entity/ in some projects. Both are valid — the convention varies by team. Inside it is always the same — @Entity classes only.
NOTE : As your app grows you will have many files in each folder but the pattern never changes. Every feature always has exactly these 4 files — Model, Repository, Service, Controller.
-----------------------------------------------------------------------------------------------------------------------------
An Entity is a plain Java class that represents a database table. Every instance of the class represents a row in that table. Every field in the class represents a column. You mark the class with @Entity and JPA automatically handles creating the table, inserting rows, fetching rows, and mapping them back to Java objects.
Java World Database World
Entity class → Table
Field → Column
Object → Row
Field value → Cell value
Before JPA when you fetched a row from the database you had to manually map every column to a Java field :
// Without Entity — manual mapping, tedious and error prone
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
User user = new User();
user.setId(rs.getInt("id"));
user.setName(rs.getString("name"));
user.setEmail(rs.getString("email"));
user.setAge(rs.getInt("age"));
With Entity JPA does this mapping automatically. You just work with Java objects and JPA handles converting them to database rows and back.
Creating a Basic Entity
You take a plain Java class and add JPA annotations to it and convert it into an JPA entity that maps to an actual database table.
import jakarta.persistence.*;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
private String email;
private int age;
// No-args constructor — required by JPA
public User() { }
// Parameterized constructor
public User(String name, String email, int age) {
this.name = name;
this.email = email;
this.age = age;
}
// Getters and Setters
public int getId() { return id; }
public String getName() { return name; }
public String getEmail() { return email; }
public int getAge() { return age; }
public void setId(int id) { this.id = id; }
public void setName(String name) { this.name = name; }
public void setEmail(String email) { this.email = email; }
public void setAge(int age) { this.age = age; }
}
This single class automatically creates this table in MySQL :
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255),
email VARCHAR(255),
age INT
);
Important Rules for Entity
- Rule 1 — Every Entity must have
@Entity annotation.
- Rule 2 — Every Entity must have exactly one
@Id field.
- Rule 3 — Every Entity must have a no-args constructor. JPA uses it to create objects when fetching rows from the database. If you forget this JPA will throw an error at startup.
- Rule 4 — Never set the
id field manually. Let JPA and the database handle it through @GeneratedValue.
- Rule 5 — Entity fields should have getters and setters. JPA uses them to read and set values.
Below are some commonly used Entity annotations that help create the table mapping :
@Entity — you put this on a class to tell JPA "this class is a database table." Without it JPA does not know this class has anything to do with the database.
@Table — you put this to tell JPA what the table should be named in the database. If you skip it JPA just uses the class name. So a class called User becomes a table called user. Most developers add this just to be explicit about the table name.
@Id — every table needs a primary key. You put this on whichever field is the primary key. JPA needs to know which field uniquely identifies each row.
@Column — by default JPA uses your field name as the column name and applies no constraints. If you want to rename the column, make it NOT NULL, make it UNIQUE, or limit its length — you use @Column. Think of it as customizing that specific column in the database.
@Transient — sometimes you need a field in your Java class that should NOT be saved to the database. For example a calculated field like fullName that is just firstName + lastName. You put @Transient on it and JPA ignores it completely.
@CreationTimestamp — automatically sets the current date and time when a record is first created. You never set it manually — Hibernate handles it.
@UpdateTimestamp — automatically updates to the current date and time every time the record is saved. You never set it manually — Hibernate handles it.
Why does JPA need getters and setters?
When JPA fetches a row from the database it needs to set values into your Java object. It does this through setters. When JPA needs to read values from your Java object to save to the database, it does this through getters. Without getters and setters JPA cannot read or write your field values and throws an error.
Database row fetched
↓
JPA calls user.setId(1)
JPA calls user.setName("Deepesh")
JPA calls user.setEmail("deepesh@gmail.com")
↓
Java object is ready
------------------------------------------------------------------------------------------
You call userRepository.save(user)
↓
JPA calls user.getId()
JPA calls user.getName()
JPA calls user.getEmail()
↓
JPA builds INSERT/UPDATE SQL with those values
NOTE : This is also why JPA needs a no-args constructor — it first creates an empty object using new User() and then calls setters to fill in the values one by one.
What if your database has no constraints like primary key or foreign key?
Technically JPA requires at least a primary key — the @Id field. Without it JPA has no way to uniquely identify a row and cannot perform updates or deletes correctly. But for everything else like NOT NULL, UNIQUE, foreign keys — these are optional. JPA does not force you to have them.
When you have a single table with just a primary key JPA works perfectly fine. The @Id is the only thing JPA truly requires because it needs a way to uniquely identify each row for updates and deletes.
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name; // no constraints — completely fine
private String email; // no constraints — completely fine
}
The real question is — when two tables are related to each other, how does JPA know which column links them? In a normal database a posts table has a user_id column that references the users table. JPA needs to know about this relationship to be able to join the tables when fetching data. You define this using relationship annotations directly on your Entity fields :
// User — one user has many posts
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
@OneToMany(mappedBy = "user") // one user has many posts
private List<Post> posts;
}
// Post — many posts belong to one user
@Entity
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String title;
@ManyToOne
@JoinColumn(name = "user_id") // user_id column links post to user
private User user;
}
The above mapping create/map to the following table structures even though their is not foreign key constraint present in the tables still JPA works.
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255)
);
CREATE TABLE posts (
id INT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(255),
user_id INT -- linking column, created by JPA automatically
);
```
---
### Foreign Key Constraint — Optional Safety Net
Notice that JPA created the `user_id` column but did NOT
add a `FOREIGN KEY` constraint to it. The column exists and JPA
uses it to join the tables when fetching data, but the database
itself does not enforce the relationship.
This means :
```
With foreign key constraint Without foreign key constraint
↓ ↓
Database enforces relationship JPA still works perfectly
Cannot insert post with But database does not care
invalid user_id You could insert a post with
Database throws error an invalid user_id and database
would not complain
JPA handles relationships at the application level regardless of whether the database has an actual foreign key constraint. The constraint is just an optional extra layer of safety at the database level.
NOTE : In real production projects you should always have foreign key constraints in your database for data integrity. But JPA works either way. The relationship annotations tell JPA how to join tables — the database constraint is just a safety net on top of that.
Entity Annotations in Detail
@Entity is the most fundamental annotation in JPA. It converts a plain Java class into a database table. Without it JPA completely ignores the class — it does not create a table, does not save data, does not fetch anything. Nothing happens.
When Spring Boot starts up and sees a class marked with @Entity it tells Hibernate — "create a table for this class in the database and manage it."
// Without @Entity — just a regular Java class, JPA ignores it
public class User {
private int id;
private String name;
}
// With @Entity — JPA creates a table for this class
@Entity
public class User {
@Id
private int id;
private String name;
}
NOTE : Every class marked with @Entity must also have a field marked with @Id. JPA needs to know the primary key to uniquely identify rows. If you forget @Id JPA throws an error at startup.
2] @Table
@Table specifies the name of the table in the database. This annotation is optional. If you skip it JPA uses the class name as the table name — User becomes user, UserProfile becomes user_profile.
Most developers always add @Table to be explicit and avoid confusion between the Java class name and the database table name.
@Entity
@Table(name = "users") // table name is "users" in database
public class User { }
@Entity // no @Table — table name defaults to "user"
public class User { }
@Table also lets you add table level constraints like a unique constraint across multiple columns :
@Entity
@Table(
name = "users",
uniqueConstraints = {
// combination of email and phone must be unique
// two users cannot have both the same email AND same phone
@UniqueConstraint(columnNames = {"email", "phone"})
}
)
public class User { }
3] @Id
@Id marks which field is the primary key of the table. Every Entity must have exactly one field marked with @Id. JPA uses this field to uniquely identify each row — it is how JPA knows which row to update, delete, or fetch.
@Entity
public class User {
@Id // this field is the primary key
private int id;
private String name;
}
The @Id field can be of any type — int, long, Integer, Long, String, UUID etc. The most common is int or Long for auto incremented ids.
NOTE : You never set the @Id field manually when creating a new object. You let @GeneratedValue handle it automatically. The id is assigned by the database after the INSERT and JPA reads it back into your object.
NOTE : If your table has no primary key but has a column with unique values like email or username, just put @Id on that column. JPA uses it as the identifier.
4] @GeneratedValue
@GeneratedValue tells JPA how to generate the primary key value automatically. Without it you would have to set the id manually every time you create a new object. There are 4 strategies :
// IDENTITY — uses database AUTO_INCREMENT
// Most common strategy, always use this with MySQL
// Database assigns the id automatically on INSERT
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
// AUTO — JPA picks the best strategy for the database automatically
// Not recommended — behavior varies across databases
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private int id;
// SEQUENCE — uses a database sequence object to generate ids
// Common in Oracle and PostgreSQL databases
// Requires a sequence to exist in the database
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private int id;
// TABLE — uses a separate database table to track and generate ids
// Slowest strategy, rarely used in practice
// Database independent but has significant performance overhead
@Id
@GeneratedValue(strategy = GenerationType.TABLE)
private int id;
NOTE : Always use GenerationType.IDENTITY with MySQL. It maps directly to AUTO_INCREMENT and is the simplest and most efficient option. You will use this in every Spring Boot project with MySQL.
5] @Column
@Column customizes the properties of a specific column in the database. It is completely optional — if you skip it JPA uses the field name as the column name with default settings. You only add it when you need to customize something about that column.
@Column(
name = "full_name", // rename column — "name" field becomes "full_name" column
nullable = false, // NOT NULL constraint — column cannot be empty
unique = true, // UNIQUE constraint — no duplicate values allowed
length = 100, // VARCHAR(100) — only applies to String fields
updatable = false, // column cannot be changed after first INSERT
insertable = true, // column can be set on INSERT — default is true
columnDefinition = "TEXT" // override the column type completely
)
private String name;
6] @Transient
@Transient marks a field that should NOT be saved to the database. JPA completely ignores this field — it does not create a column for it, does not save it, does not fetch it. Use it for calculated fields, temporary values, or anything you need in Java but do not want stored in the database.
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String firstName;
private String lastName;
@Transient // NOT saved to database
private String fullName; // calculated from firstName + lastName
@Transient // NOT saved to database
private String authToken; // temporary value, only needed during request
public String getFullName() {
return firstName + " " + lastName; // calculated at runtime, not from database
}
}
7] @Enumerated
@Enumerated is used when a field is a Java enum and you want to save it to the database. Without it JPA saves the position number of the enum value (0, 1, 2) which is dangerous — if you ever add or reorder enum values your existing data becomes wrong. With EnumType.STRING JPA saves the actual name which is always safe.
// Define the enum
public enum Role {
USER, ADMIN, MODERATOR
}
public enum Status {
ACTIVE, INACTIVE, BANNED
}
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
// EnumType.STRING — saves "USER", "ADMIN", "MODERATOR" as string in database
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Role role;
// EnumType.ORDINAL — saves 0, 1, 2 as number — NEVER use this
@Enumerated(EnumType.ORDINAL)
private Status status;
}
NOTE : Always use EnumType.STRING. It is slightly less efficient because it stores a string instead of a number, but the safety and readability it provides far outweighs the tiny performance difference.
8] @CreationTimestamp
@CreationTimestamp is a Hibernate annotation that automatically sets the field to the current date and time when a record is first created. You never set it manually — Hibernate handles it automatically when you call save() for the first time. You should always combine it with @Column(updatable = false) so the value never changes after the first insert.
import org.hibernate.annotations.CreationTimestamp;
import java.time.LocalDateTime;
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
@CreationTimestamp // Hibernate sets this automatically on INSERT
@Column(updatable = false) // never allow this to be changed after creation
private LocalDateTime createdAt;
}
User user = new User("Deepesh", "deepesh@gmail.com");
userRepository.save(user);
// createdAt is automatically set to current timestamp by Hibernate
// you never touch this field
9] @UpdateTimestamp
@UpdateTimestamp is a Hibernate annotation that automatically updates the field to the current date and time every time the record is saved. Whether you update the name, email, or any other field — Hibernate automatically updates this timestamp. You never set it manually.
import org.hibernate.annotations.UpdateTimestamp;
import java.time.LocalDateTime;
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
@CreationTimestamp
@Column(updatable = false)
private LocalDateTime createdAt; // set once on creation, never changes
@UpdateTimestamp
private LocalDateTime updatedAt; // updated automatically on every save
}
User user = userRepository.findById(1).get();
user.setName("Deepesh Updated");
userRepository.save(user);
// updatedAt is automatically set to current timestamp by Hibernate
// createdAt remains unchanged
NOTE : @CreationTimestamp and @UpdateTimestamp come from Hibernate (org.hibernate.annotations) not from JPA (jakarta.persistence). This means they only work when Hibernate is the JPA implementation — which is always the case in Spring Boot.
10] @OneToMany
@OneToMany defines a one to many relationship between two entities. One record in the parent table is related to many records in the child table. For example one User has many Posts, one Order has many Items.
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
// One User has many Posts
// mappedBy = "user" tells JPA that the Post entity owns this relationship
// through its "user" field
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Post> posts;
}
@OneToMany properties
mappedBy — tells JPA which field in the child entity owns the relationship. This prevents JPA from creating an unnecessary junction table.cascade — defines what happens to child records when the parent is saved or deleted :
CascadeType.ALL // all operations cascade — save, update, delete
CascadeType.PERSIST // only save cascades
CascadeType.MERGE // only update cascades
CascadeType.REMOVE // only delete cascades — deleting User deletes all Posts
CascadeType.REFRESH // only refresh cascades
fetch — defines when related data is loaded from the database :
FetchType.LAZY // posts are NOT loaded when User is fetched — loaded only when accessed
// default for @OneToMany — always use this
FetchType.EAGER // posts ARE loaded immediately when User is fetched
// causes performance problems — avoid unless necessary
NOTE : Always use FetchType.LAZY with @OneToMany. With EAGER every time you fetch a single user JPA also fetches ALL their posts from the database even if you do not need them. This causes massive performance problems in production.
11] @ManyToOne
@ManyToOne defines a many to one relationship. Many records in the child table are related to one record in the parent table. This is always the owning side of a @OneToMany relationship — meaning the foreign key column lives in this table.
@Entity
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String title;
private String content;
// Many Posts belong to one User
// This is the owning side — the foreign key (user_id) lives in the posts table
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id") // name of the foreign key column in posts table
private User user;
}
NOTE : @ManyToOne is always the owning side of the relationship. The owning side is the one that has the foreign key column in its table. The @OneToMany side uses mappedBy to point back to the owning side.
Example] Below is complete Entity using all the above annotations. Imagine we are building a Blog Application. We have users who write posts and each post has a category.
import jakarta.persistence.*;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.LocalDateTime;
import java.util.List;
// ============================================================
// ENUMS
// ============================================================
enum Role {
USER, ADMIN, MODERATOR
}
enum Status {
ACTIVE, INACTIVE, BANNED
}
// ============================================================
// USER ENTITY
// ============================================================
@Entity
@Table(
name = "users",
uniqueConstraints = @UniqueConstraint(columnNames = "email")
)
public class User {
// Primary key — auto incremented by database
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
// Renamed column, NOT NULL, max 100 chars
@Column(name = "full_name", nullable = false, length = 100)
private String name;
// NOT NULL, UNIQUE — no two users can have same email
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private int age;
// Saves "USER", "ADMIN", "MODERATOR" as string in database
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Role role;
// Saves "ACTIVE", "INACTIVE", "BANNED" as string in database
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Status status;
// Automatically set by Hibernate when record is created
// updatable = false — never changes after first INSERT
@CreationTimestamp
@Column(updatable = false)
private LocalDateTime createdAt;
// Automatically updated by Hibernate on every save
@UpdateTimestamp
private LocalDateTime updatedAt;
// NOT saved to database — calculated field
@Transient
private String displayName;
// One User has many Posts
// cascade = ALL — deleting user also deletes all their posts
// fetch = LAZY — posts are NOT loaded when user is fetched
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Post> posts;
// No-args constructor — required by JPA
public User() { }
public User(String name, String email, int age, Role role, Status status) {
this.name = name;
this.email = email;
this.age = age;
this.role = role;
this.status = status;
}
// Getters
public int getId() { return id; }
public String getName() { return name; }
public String getEmail() { return email; }
public int getAge() { return age; }
public Role getRole() { return role; }
public Status getStatus() { return status; }
public LocalDateTime getCreatedAt() { return createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public List<Post> getPosts() { return posts; }
public String getDisplayName() { return name + " (" + role + ")"; } // calculated
// Setters
public void setName(String name) { this.name = name; }
public void setEmail(String email) { this.email = email; }
public void setAge(int age) { this.age = age; }
public void setRole(Role role) { this.role = role; }
public void setStatus(Status status) { this.status = status; }
public void setPosts(List<Post> posts) { this.posts = posts; }
}
// ============================================================
// POST ENTITY
// ============================================================
@Entity
@Table(name = "posts")
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@Column(nullable = false, length = 200)
private String title;
// TEXT type — for long content
@Column(nullable = false, columnDefinition = "TEXT")
private String content;
// Saves "DRAFT", "PUBLISHED" as string
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private PostStatus status;
@CreationTimestamp
@Column(updatable = false)
private LocalDateTime createdAt;
@UpdateTimestamp
private LocalDateTime updatedAt;
// NOT saved to database — calculated field
@Transient
private String summary;
// Many Posts belong to one User
// foreign key column "user_id" lives in posts table
@ManyToOne(fetch = FetchType.LAZY)
@Column(name = "user_id", nullable = false)
private User user;
public Post() { }
public Post(String title, String content, PostStatus status, User user) {
this.title = title;
this.content = content;
this.status = status;
this.user = user;
}
// Getters
public int getId() { return id; }
public String getTitle() { return title; }
public String getContent() { return content; }
public PostStatus getStatus() { return status; }
public LocalDateTime getCreatedAt() { return createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public User getUser() { return user; }
public String getSummary() { return content.substring(0, 100) + "..."; } // calculated
// Setters
public void setTitle(String title) { this.title = title; }
public void setContent(String content) { this.content = content; }
public void setStatus(PostStatus status) { this.status = status; }
public void setUser(User user) { this.user = user; }
}
enum PostStatus {
DRAFT, PUBLISHED, ARCHIVED
}
What tables JPA creates in MySQL based on the above entity mapping is given below.
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
full_name VARCHAR(100) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
age INT NOT NULL,
role VARCHAR(255) NOT NULL,
status VARCHAR(255) NOT NULL,
created_at DATETIME,
updated_at DATETIME
-- displayName is NOT here — @Transient
);
CREATE TABLE posts (
id INT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(200) NOT NULL,
content TEXT NOT NULL,
status VARCHAR(255) NOT NULL,
created_at DATETIME,
updated_at DATETIME,
user_id INT NOT NULL -- foreign key linking to users table
-- summary is NOT here — @Transient
);
-----------------------------------------------------------------------------------------------------------------------------
You have your Entities mapped to database tables. Now you need a way to talk to those tables — fetch, save, update, delete. That is the Repository. It sits between your Service and your database with one job — database operations. No business logic, no HTTP handling. Just data in and data out.
In Spring Boot the Repository is always an interface that extends JpaRepository. Spring generates the entire implementation automatically at startup. Zero SQL, zero boilerplate.
Without Spring you would write every database function manually :
// Without Spring — you write everything manually
public User getUserById(int id) {
Connection conn = DriverManager.getConnection(url, username, password);
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
stmt.setInt(1, id);
ResultSet rs = stmt.executeQuery();
User user = new User();
user.setId(rs.getInt("id"));
user.setName(rs.getString("name"));
conn.close();
return user;
}
Think of it like a userStore.js file in Node.js that stores all the database functions for a specific entity. In Spring Boot it is the exact same concept — a dedicated place that holds all database operations for one Entity. The only difference is Spring writes all the common functions for you automatically.
A Repository in Spring Boot is always an interface — never a class. You just declare the interface, extend JpaRepository, and Spring generates the entire implementation automatically at startup. You never write public class UserRepository.
@Repository
public interface UserRepository extends JpaRepository<User, Integer> {
// User — the Entity this repository manages
// Integer — the type of the primary key (id field type)
}
That is literally all you need. Just by writing these 3 lines Spring gives you a fully working database layer with all basic operations ready to use. JpaRepository is a generic interface provided by Spring Data JPA.
When you extend it you pass 2 type parameters telling Spring which Entity to manage and what type the primary key is :
JpaRepository<EntityType, PrimaryKeyType>. // SYNTAX
JpaRepository<User, Integer> // managing User entity, primary key is int
JpaRepository<Product, Long> // managing Product entity, primary key is long
JpaRepository<Order, String> // managing Order entity, primary key is String
Spring reads these type parameters at startup and generates a complete implementation that knows exactly which table to query, which columns to map, and how to convert database rows back into your Java objects.
NOTE : You never inject the Repository directly into the Controller. Always inject it into the Service. The Controller only knows about the Service, the Service only knows about the Repository. This is the layered architecture rule.
NOTE : Spring generates the Repository implementation at startup using Java Reflection and Proxy classes. You never see this generated code — it happens entirely behind the scenes. This is one of the most powerful features of Spring Data JPA.
@Repository
@Repository is a Spring annotation that marks an interface or class as a data access component. It tells Spring two things :
- 1] Make this a Bean — Spring creates and manages this as a Bean in the Application Context so it can be injected anywhere with
@Autowired. - 2] Enable exception translation — Spring automatically catches database specific exceptions (like Hibernate exceptions, JDBC exceptions) and converts them into Spring's own consistent exception hierarchy. This means regardless of which database you use, you always get the same type of exceptions in your code.
// Without @Repository — Spring does not manage this, cannot be injected
public interface UserRepository extends JpaRepository<User, Integer> { }
// With @Repository — Spring manages this as a Bean
@Repository
public interface UserRepository extends JpaRepository<User, Integer> { }
// Now you can inject it anywhere
@Service
public class UserService {
@Autowired
private UserRepository userRepository; // Spring injects it automatically
}
Interesting fact — @Repository is actually optional here
When you extend JpaRepository, Spring Data JPA automatically detects and registers your interface as a Bean even without @Repository. Spring Data JPA has its own scanning mechanism that finds all interfaces extending JpaRepository at startup.
But most developers still add @Repository because :
- It makes the intent clear — this is a repository class
- It enables the exception translation feature
- It is consistent with the rest of the layered architecture where every layer has its annotation —
@RestController, @Service, @Repository
When you extend JpaRepository you get access to 3 types of methods to interact with your database. Think of them as 3 levels — each level gives you more control and more flexibility than the previous one. You start with what Spring gives you for free, and as your queries get more complex you move to the next level.
- Built-in Methods are provided by
JpaRepository automatically with zero code. These cover the most common operations every application needs — fetch all, fetch by id, save, delete. You never write these, they just exist.
- Derived Query Methods are methods you define yourself by following a naming convention. You write the method name, Spring reads it and generates the SQL automatically. No SQL needed from your side — Spring figures it out from the name.
- Custom @Query Methods are for when the method name approach gets too complex. You write both the method name and the actual query yourself — either in JPQL or raw SQL. Full control over exactly what query runs.
Repository Methods
│
├── 1] Built-in JPA Methods
│ Provided automatically by JpaRepository
│ No code needed at all
│ │
│ ├── findAll()
│ ├── findById()
│ ├── save()
│ ├── deleteById()
│ └── count()
│
├── 2] Derived Query Methods
│ You write the method name following JPA naming convention
│ Spring reads the name and generates SQL automatically
│ No SQL needed
│ │
│ ├── findByEmail()
│ ├── findByCity()
│ ├── findByAgeGreaterThan()
│ └── findByNameAndCity()
│
└── 3] Custom @Query Methods
You write the method name AND the query yourself
Use when method name gets too complex
Can write JPQL or raw SQL
│
├── @Query("SELECT u FROM User u WHERE ...") ← JPQL
└── @Query(value = "SELECT * FROM users WHERE ...", nativeQuery = true) ← raw SQL
These come from JpaRepository automatically. You do not write a single line of code — they are ready to use the moment you extend JpaRepository. These cover the most common database operations you will need in any application.
// All of these work immediately — no code needed
// READ
userRepository.findAll() // SELECT * FROM users
userRepository.findById(1) // SELECT * FROM users WHERE id = ? → returns Optional<User>
userRepository.existsById(1) // check if user exists → returns boolean
userRepository.count() // SELECT COUNT(*) FROM users → returns long
// CREATE / UPDATE
userRepository.save(user) // INSERT if id is 0 or null, UPDATE if id has value
userRepository.saveAll(usersList) // INSERT or UPDATE multiple users at once
// DELETE
userRepository.deleteById(1) // DELETE FROM users WHERE id = ?
userRepository.delete(user) // DELETE by user object
userRepository.deleteAll() // DELETE FROM users — deletes everything
How save() decides INSERT vs UPDATE
save() is the most important method in JpaRepository. It handles both creating and updating a record in one single method. You do not need separate insert() and update() methods — save() figures out which one to do automatically.
It checks the id field of the object you pass to it to decide if to perform insert or update :
id is 0 or null → record does not exist yet → INSERT
id has a value → record already exists → UPDATE
When you create a new object you never set the id. It defaults to 0. JPA sees id = 0 and does an INSERT. When you fetch an existing record it already has an id. You change some fields and call save(). JPA sees the id has a value and does an UPDATE.
---------------------INSERT--------------------------------------------------
// Create a new user — id is not set, defaults to 0
User newUser = new User("Deepesh", "deepesh@gmail.com", 20);
System.out.println(newUser.getId()); // 0 — not set yet
// save() sees id = 0 — does INSERT
userRepository.save(newUser);
// SQL : INSERT INTO users (name, email, age) VALUES ('Deepesh', 'deepesh@gmail.com', 20)
// After save — database assigned the id, JPA reads it back into your object
System.out.println(newUser.getId()); // 1 — assigned by database
-----------------------UPDATE------------------------------------------------
// Fetch existing user — id is 1
User existingUser = userRepository.findById(1).get();
System.out.println(existingUser.getId()); // 1
System.out.println(existingUser.getName()); // Deepesh
// Change some fields
existingUser.setName("Deepesh Updated");
existingUser.setAge(21);
// save() sees id = 1 — does UPDATE
userRepository.save(existingUser);
// SQL : UPDATE users SET name = 'Deepesh Updated', age = 21 WHERE id = 1
Derived query methods are methods you define in your Repository by following a specific naming convention. You just write the method name — Spring reads it word by word, figures out what SQL query you want, and generates it automatically. No SQL needed from your side at all.
Spring Data JPA treats your method name as a sentence that describes the query. It parses the name and translates it directly into SQL :
findByCity(String city)
Spring reads :
"find" → I want to SELECT
"By" → WHERE condition follows
"City" → the condition is on the city column
Generates :
SELECT * FROM users WHERE city = ?
The structure of a derived method name
Every derived method name has 3 parts :
- Subject — what operation to perform. Can be
find, count, exists, delete.
- By — separator that tells Spring the conditions start here.
- Condition — which field to filter on and how. This is where you use keywords like
GreaterThan, Between, Containing etc.
find By City
↓ ↓ ↓
Subject Separator Condition
(what to do) (where) (filter criteria)
Below are some common keywords spring understands for different operations :
// Keywords for Subject
find / get / read → SELECT — returns List or Optional
count → SELECT COUNT — returns long
exists → SELECT COUNT — returns boolean
delete → DELETE — returns void
// Keywords for Condition
By → WHERE
And → AND
Or → OR
Not → !=
GreaterThan → >
GreaterThanEqual → >=
LessThan →
LessThanEqual → <=
Between → BETWEEN ? AND ?
Containing → LIKE '%?%'
StartingWith → LIKE '?%'
EndingWith → LIKE '%?'
In → IN (?)
NotIn → NOT IN (?)
IsNull → IS NULL
IsNotNull → IS NOT NULL
True → = true
False → = false
OrderBy + Asc/Desc → ORDER BY ? ASC/DESC
Example] Below is an extensive exampel where we only define derived methods and can just directly start using them, JPA generates SQL for them automatically on startup.
@Repository
public interface UserRepository extends JpaRepository<User, Integer> {
// -----------------------------------------------
// BASIC FIND
// -----------------------------------------------
Optional<User> findByEmail(String email);
// SELECT * FROM users WHERE email = ?
List<User> findByCity(String city);
// SELECT * FROM users WHERE city = ?
List<User> findByName(String name);
// SELECT * FROM users WHERE name = ?
// -----------------------------------------------
// COMPARISON
// -----------------------------------------------
List<User> findByAgeGreaterThan(int age);
// SELECT * FROM users WHERE age > ?
List<User> findByAgeLessThan(int age);
// SELECT * FROM users WHERE age < ?
List<User> findByAgeGreaterThanEqual(int age);
// SELECT * FROM users WHERE age >= ?
List<User> findByAgeBetween(int min, int max);
// SELECT * FROM users WHERE age BETWEEN ? AND ?
// -----------------------------------------------
// MULTIPLE CONDITIONS
// -----------------------------------------------
List<User> findByNameAndCity(String name, String city);
// SELECT * FROM users WHERE name = ? AND city = ?
List<User> findByNameOrCity(String name, String city);
// SELECT * FROM users WHERE name = ? OR city = ?
List<User> findByAgeGreaterThanAndCity(int age, String city);
// SELECT * FROM users WHERE age > ? AND city = ?
// -----------------------------------------------
// STRING MATCHING
// -----------------------------------------------
List<User> findByNameContaining(String keyword);
// SELECT * FROM users WHERE name LIKE '%keyword%'
List<User> findByNameStartingWith(String prefix);
// SELECT * FROM users WHERE name LIKE 'prefix%'
List<User> findByNameEndingWith(String suffix);
// SELECT * FROM users WHERE name LIKE '%suffix'
// -----------------------------------------------
// NULL CHECKS
// -----------------------------------------------
List<User> findByPhoneIsNull();
// SELECT * FROM users WHERE phone IS NULL
List<User> findByPhoneIsNotNull();
// SELECT * FROM users WHERE phone IS NOT NULL
// -----------------------------------------------
// BOOLEAN
// -----------------------------------------------
List<User> findByActiveTrue();
// SELECT * FROM users WHERE active = true
List<User> findByActiveFalse();
// SELECT * FROM users WHERE active = false
// -----------------------------------------------
// SORTING
// -----------------------------------------------
List<User> findByCityOrderByNameAsc(String city);
// SELECT * FROM users WHERE city = ? ORDER BY name ASC
List<User> findByCityOrderByAgeDesc(String city);
// SELECT * FROM users WHERE city = ? ORDER BY age DESC
// -----------------------------------------------
// COUNT AND EXISTS
// -----------------------------------------------
long countByCity(String city);
// SELECT COUNT(*) FROM users WHERE city = ?
boolean existsByEmail(String email);
// SELECT COUNT(*) FROM users WHERE email = ? → true or false
// -----------------------------------------------
// DELETE
// -----------------------------------------------
void deleteByCity(String city);
// DELETE FROM users WHERE city = ?
// -----------------------------------------------
// IN
// -----------------------------------------------
List<User> findByAgeIn(List<Integer> ages);
// SELECT * FROM users WHERE age IN (?, ?, ?)
List<User> findByCityIn(List<String> cities);
// SELECT * FROM users WHERE city IN (?, ?, ?)
}
NOTE : Spring generates the SQL from the method name at startup, not at runtime. If you make a typo in the method name Spring throws an error when the app starts, not when the method is called. This is actually a good thing — you catch errors early.
NOTE : The field names in the method name must exactly match the field names in your Entity class, not the column names in the database. So if your Entity has private String fullName you write findByFullName() not findByFull_name().
When to stop using derived methods
Derived methods are great for simple to medium queries. But they have a limit — when conditions get too complex the method name becomes unreadable :
// This is getting too long and hard to read
List<User> findByAgeGreaterThanAndCityAndActiveTrue(int age, String city);
// This is where you switch to @Query instead
@Query("SELECT u FROM User u WHERE u.age > :age AND u.city = :city AND u.active = true")
List<User> findActiveAdultsInCity(@Param("age") int age, @Param("city") String city);
When derived method names get too complex or you need a very specific query that Spring cannot generate from a method name, you write the query yourself using @Query. You have full control over exactly what SQL runs.
With derived methods Spring writes the SQL for you. With @Query you write the SQL yourself. You still define a method in your Repository interface — but now you also provide the actual query that should run when that method is called.
// Derived method — works fine for simple queries
List<User> findByCity(String city);
// But what about this? Method name becomes unreadable
List<User> findByAgGreaterThanAndCityAndActiveTrueOrderByNameAsc(int age, String city);
// @Query — same query, clean and readable
@Query("SELECT u FROM User u WHERE u.age > :age AND u.city = :city AND u.active = true ORDER BY u.name ASC")
List<User> findActiveAdultsInCity(@Param("age") int age, @Param("city") String city);
JPQL — Java Persistence Query Language
JPQL is JPA's own query language. It was created because JPA works with Java objects, not database tables directly. When you write a query you are querying your Java objects not the database. Hibernate then takes your JPQL query and translates it into the actual SQL for whatever database you are using.
This makes JPQL database independent — the same JPQL query works on MySQL, PostgreSQL, Oracle, or any other database without any changes. Hibernate handles the translation.
In SQL you think about tables and columns. In JPQL you think about your Entity classes and their fields :
SQL thinks about the database :
SELECT * FROM users WHERE full_name = 'Deepesh'
↑ ↑
table name column name
JPQL thinks about your Java objects :
SELECT u FROM User u WHERE u.name = 'Deepesh'
↑ ↑
Entity class field name
NOTE : JPQL is always case sensitive for Entity class names and field names. User is correct, user is wrong. name is correct, Name is wrong. This is a very common source of errors when writing JPQL for the first time.
When you write a @Query you have 2 choices for how to write the query :
- JPQL — uses your Java class names and field names. Looks like SQL but refers to your Entity not the database table.
- Raw SQL — actual SQL using real table and column names. Exactly what you would write in MySQL Workbench.
// Your Entity
@Entity
@Table(name = "users")
public class User {
private String name; // field name
// maps to column "full_name" in database
@Column(name = "full_name")
...
}
// JPQL — uses "User" (class name) and "name" (field name)
@Query("SELECT u FROM User u WHERE u.name = :name")
// Raw SQL — uses "users" (table name) and "full_name" (column name)
@Query(value = "SELECT * FROM users WHERE full_name = :name", nativeQuery = true)
```
The key difference is simple :
```
JPQL → thinks in Java → use class names and field names
Raw SQL → thinks in SQL → use table names and column names
NOTE : Add nativeQuery = true to tell Spring this is raw SQL not JPQL. Use when the query is too complex for JPQL or when you need database specific features.
@Param — passing values into the query
When you write a @Query you need a way to pass values from your method parameters into the query. @Param is how you do that.
In your query you write a placeholder starting with : like :age or :city. Then in your method parameters you use @Param("age") and @Param("city") to tell Spring which parameter maps to which placeholder.
// Simple example — one parameter
@Query("SELECT u FROM User u WHERE u.email = :email")
Optional<User> findByEmail(@Param("email") String email);
// When called with : findByEmail("deepesh@gmail.com")
// Spring replaces : :email → "deepesh@gmail.com"
// Final SQL : SELECT * FROM users WHERE email = 'deepesh@gmail.com'
// Multiple parameters
@Query("SELECT u FROM User u WHERE u.age > :age AND u.city = :city")
List<User> findAdultsInCity(@Param("age") int age, @Param("city") String city);
NOTE : The name you give in `@Param("name")` must exactly match the `:name` placeholder in your query. If you write `@Param("age")` but use `:userAge` in the query Spring cannot find the mapping and throws an error at runtime.
@Modifying
Spring Data JPA was designed primarily for reading data. When you write a @Query Spring assumes by default that it is a SELECT query. It sets up everything internally to handle a read operation — it expects your query to return data back.
When you write an UPDATE or DELETE query and do not tell Spring about it, Spring still tries to handle it as a SELECT. This causes a conflict — a SELECT returns rows, but UPDATE and DELETE do not return rows, they modify them. Spring gets confused and throws an error.
// ❌ Without @Modifying — Spring thinks this is SELECT, throws error
@Query("UPDATE User u SET u.active = false WHERE u.id = :id")
int deactivateUser(@Param("id") int id);
// ✅ With @Modifying — Spring knows this modifies data, works correctly
@Modifying
@Query("UPDATE User u SET u.active = false WHERE u.id = :id")
int deactivateUser(@Param("id") int id);
@Modifying is a simple signal to Spring that says "this is not a SELECT query — this query modifies data." When Spring sees @Modifying it switches from read mode to write mode internally and uses executeUpdate() instead of executeQuery() under the hood.
NOTE : @Modifying is only needed when you write your own UPDATE or DELETE through @Query. Built-in methods like deleteById() and save() already handle this internally — you never add @Modifying to those.
@Modifying has an extra feature called clearAutomatically. When you update a record directly through @Query the change happens in the database but JPA might still have the old version of the object cached in memory. If you fetch that object right after the update you might get stale data instead of the updated data.
clearAutomatically = true tells JPA to clear its cache after the query runs so the next fetch always gets fresh data from the database :
// Without clearAutomatically — might get stale cached data after update
@Modifying
@Transactional
@Query("UPDATE User u SET u.active = false WHERE u.id = :id")
int deactivateUser(@Param("id") int id);
// With clearAutomatically — cache cleared, always get fresh data
@Modifying(clearAutomatically = true)
@Transactional
@Query("UPDATE User u SET u.active = false WHERE u.id = :id")
int deactivateUser(@Param("id") int id);
NOTE : Always add clearAutomatically = true when using @Modifying with UPDATE queries. It prevents hard to find bugs where your code reads stale data after an update.
@Transactional
@Transactional is a Spring annotation that wraps a method in a database transaction. When the method starts a transaction begins. When the method completes successfully the transaction is committed — all changes are saved to the database. If anything throws an exception the transaction is rolled back — all changes are undone.
Method starts
↓
Transaction begins
↓
Database operations run
↓
├── All succeed → Transaction commits → changes saved to database
└── Any fails → Transaction rolls back → all changes undone
NOTE : In Spring Boot @Transactional is the single most important annotation for maintaining data integrity. Any service method that performs multiple database operations should always be marked with @Transactional. Without it partial updates can leave your database in an inconsistent state.
NOTE : Spring Boot's JpaRepository methods like save(), deleteById() etc. are already @Transactional internally. You only need to add @Transactional yourself when you have multiple operations that need to be grouped together in one transaction.
// On a Repository @Query method
@Modifying
@Transactional
@Query("UPDATE User u SET u.active = false WHERE u.id = :id")
int deactivateUser(@Param("id") int id);
// On a Service method — more common in real projects
@Service
public class BankService {
@Autowired
private AccountRepository accountRepository;
@Transactional // if anything fails inside, all changes are rolled back
public void transferMoney(int fromId, int toId, double amount) {
// Step 1 — deduct from sender
accountRepository.deductBalance(fromId, amount);
// Step 2 — add to receiver
// if this throws an exception Step 1 is automatically rolled back
accountRepository.addBalance(toId, amount);
}
}
Why @Transactional on Service is better than on Repository
When you put @Transactional on a Repository method each database operation gets its own separate transaction. They are not connected to each other. If you have 3 repository calls in one service method each runs in its own transaction independently.
When you put @Transactional on a Service method all repository calls inside that method share the same transaction. They are all connected. If any one of them fails all of them are rolled back together.
When @Transactional rolls back
By default @Transactional only rolls back on unchecked exceptions (RuntimeException and its subclasses). It does NOT roll back on checked exceptions.
@Transactional
public void createUser(User user) {
userRepository.save(user);
// RuntimeException (unchecked) — transaction ROLLS BACK ✅
throw new RuntimeException("Something went wrong");
// IOException (checked) — transaction does NOT roll back ❌
throw new IOException("File not found");
}
To roll back on checked exceptions you specify them explicitly :
// Roll back on any exception — checked or unchecked
@Transactional(rollbackFor = Exception.class)
public void createUser(User user) {
userRepository.save(user);
throw new IOException("This will now roll back");
}
Example] Below we use Raw queries, JPQL and Transactional methods at same time.
@Repository
public interface UserRepository extends JpaRepository<User, Integer> {
// --- Derived methods ---
List<User> findByCity(String city);
boolean existsByEmail(String email);
// --- JPQL ---
@Query("SELECT u FROM User u WHERE u.age > :age AND u.city = :city AND u.active = true")
List<User> findActiveAdultsInCity(@Param("age") int age, @Param("city") String city);
// --- Raw SQL ---
@Query(value = "SELECT * FROM users WHERE full_name LIKE %:keyword%", nativeQuery = true)
List<User> searchByName(@Param("keyword") String keyword);
// --- UPDATE with @Modifying ---
@Modifying(clearAutomatically = true)
@Transactional
@Query("UPDATE User u SET u.active = false WHERE u.id = :id")
int deactivateUser(@Param("id") int id);
// --- DELETE with @Modifying ---
@Modifying
@Transactional
@Query("DELETE FROM User u WHERE u.city = :city")
void deleteByCity(@Param("city") String city);
}
-----------------------------------------------------------------------------------------------------------------------------
Example] Below is a simple example where we define entity, repository and service.
import jakarta.persistence.*;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
// ============================================================
// ENTITY
// ============================================================
@Entity
@Table(name = "products")
class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private double price;
@Column(nullable = false)
private int stock;
public Product() { }
public Product(String name, double price, int stock) {
this.name = name;
this.price = price;
this.stock = stock;
}
public int getId() { return id; }
public String getName() { return name; }
public double getPrice() { return price; }
public int getStock() { return stock; }
public void setName(String name) { this.name = name; }
public void setPrice(double price) { this.price = price; }
public void setStock(int stock) { this.stock = stock; }
}
// ============================================================
// REPOSITORY
// ============================================================
@Repository
interface ProductRepository extends JpaRepository<Product, Integer> {
// 1] Built-in — findAll(), findById(), save(), deleteById() etc.
// 2] Derived methods
List<Product> findByName(String name);
List<Product> findByPriceLessThan(double price);
List<Product> findByStockGreaterThan(int stock);
boolean existsByName(String name);
// 3] Custom JPQL
@Query("SELECT p FROM Product p WHERE p.price < :price AND p.stock > :stock")
List<Product> findAffordableInStock(@Param("price") double price, @Param("stock") int stock);
}
// ============================================================
// SERVICE
// ============================================================
@Service
class ProductService {
@Autowired
private ProductRepository productRepository;
// Get all products
public List<Product> getAllProducts() {
return productRepository.findAll();
}
// Get product by id
public Product getProductById(int id) {
return productRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Product not found"));
}
// Create product
public Product createProduct(Product product) {
if (productRepository.existsByName(product.getName())) {
throw new RuntimeException("Product already exists");
}
return productRepository.save(product);
}
// Update product
public Product updateProduct(int id, Product updatedProduct) {
Product existing = getProductById(id);
existing.setName(updatedProduct.getName());
existing.setPrice(updatedProduct.getPrice());
existing.setStock(updatedProduct.getStock());
return productRepository.save(existing);
}
// Delete product
public void deleteProduct(int id) {
getProductById(id);
productRepository.deleteById(id);
}
// Get affordable products in stock
public List<Product> getAffordableInStock(double maxPrice, int minStock) {
return productRepository.findAffordableInStock(maxPrice, minStock);
}
}
-----------------------------------------------------------------------------------------------------------------------------
Entity Graph
Entity Graph is a JPA feature that lets you control what data gets loaded from the database. It solves the N+1 query problem by defining exactly which relationships to fetch in a single query.
The N+1 Query Problem is a performance issue where your application executes 1 query to fetch parent records, then N additional queries to fetch related data for each parent.
"N+1" means:
// WITHOUT ENTITY GRAPHS
List<Order> orders = orderRepository.findAll();
// This executes 1 query for orders:
// SELECT * FROM orders
for (Order order : orders) {
// This executes 1 query PER order!
// SELECT * FROM order_items WHERE order_id = ?
System.out.println(order.getItems().size());
}
// If you have 10 orders → 11 queries total (1 + 10)
// This is the N+1 problem!
┌─────────────────────────────────────────────────────────────────┐
│ N+1 QUERY PROBLEM │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Query 1: SELECT * FROM orders │
│ ↓ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Order 1 │ Order 2 │ Order 3 │ ... │ Order 10 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ├── Query 2: SELECT * FROM items WHERE order_id = 1 │
│ ├── Query 3: SELECT * FROM items WHERE order_id = 2 │
│ ├── Query 4: SELECT * FROM items WHERE order_id = 3 │
│ └── ... up to Query 11 │
│ │
│ Total: 11 queries for 10 orders! │
│ │
└─────────────────────────────────────────────────────────────────┘
With Entity Graph we can reduce the number of queries from N+1 to just 1 single query.
Scenario: 1000 users viewing their orders page
WITHOUT Entity Graph:
1000 users × 1001 queries = 1,001,000 queries to database
Time: ~50 seconds per user
WITH Entity Graph:
1000 users × 1 query = 1000 queries to database
Time: ~0.2 seconds per user
Difference: Your app either works or crashes!
You have two tables: Users and College
COLLEGE TABLE
┌─────┬──────────────────┬─────────┐
│ id │ name │ city │
├─────┼──────────────────┼─────────┤
│ 1 │ Harvard │ Boston │
│ 2 │ Stanford │ California│
│ 3 │ MIT │ Boston │
└─────┴──────────────────┴─────────┘
USERS TABLE
┌─────┬──────────┬────────────┐
│ id │ name │ college_id │
├─────┼──────────┼────────────┤
│ 1 │ Alice │ 1 │
│ 2 │ Bob │ 1 │
│ 3 │ Charlie │ 2 │
│ 4 │ David │ 2 │
│ 5 │ Emma │ 3 │
└─────┴──────────┴────────────┘
Step 1]: Entity Classes
// College.java
@Entity
public class College {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String city;
// One college has many users
@OneToMany(mappedBy = "college")
private List<User> users; // ← We want to load this
// Getters and setters
}
// User.java
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// Many users belong to one college
@ManyToOne
@JoinColumn(name = "college_id")
private College college;
// Getters and setters
}
Step 2]: The Problem (N+1 Queries)
@Repository
public interface CollegeRepository extends JpaRepository<College, Long> {
List<College> findAll(); // Only fetches colleges
}
@Service
public class CollegeService {
public void printCollegesWithUsers() {
// Query 1: Get all colleges
List<College> colleges = collegeRepository.findAll();
// SELECT * FROM college
for (College college : colleges) { // N = 3 colleges
// Query 2: Get users for Harvard (college_id = 1)
// Query 3: Get users for Stanford (college_id = 2)
// Query 4: Get users for MIT (college_id = 3)
System.out.println(college.getName() + ": " + college.getUsers().size());
}
// TOTAL: 1 + 3 = 4 queries for only 3 colleges!
}
}
Query 1: SELECT * FROM college → Returns 3 colleges
Query 2: SELECT * FROM user WHERE college_id = 1 → Harvard's users
Query 3: SELECT * FROM user WHERE college_id = 2 → Stanford's users
Query 4: SELECT * FROM user WHERE college_id = 3 → MIT's users
Total: 4 queries
Step 3]: Solution - Add Entity Graph
@Repository
public interface CollegeRepository extends JpaRepository<College, Long> {
// BAD - Causes N+1
List<College> findAll();
// GOOD - Fixes N+1
@EntityGraph(attributePaths = {"users"}) // ← Add this
List<College> findAllWithUsers();
}
What attributePaths = {"users"} means:
Step 4]: Use the Entity Graph
@Service
public class CollegeService {
private final CollegeRepository collegeRepository;
// BAD WAY - Causes N+1
public void printCollegesBad() {
List<College> colleges = collegeRepository.findAll(); // 1 query
for (College college : colleges) { // N queries
System.out.println(college.getName() + ": " + college.getUsers().size());
}
// Total: 1 + N queries
}
// GOOD WAY - Uses Entity Graph
public void printCollegesGood() {
// One query with JOIN
List<College> colleges = collegeRepository.findAllWithUsers(); // 1 query
for (College college : colleges) {
// Users already loaded, no extra query
System.out.println(college.getName() + ": " + college.getUsers().size());
}
// Total: 1 query only!
}
}
With Entity Graph (1 query)
-- ONE query with JOIN
SELECT * FROM college c
LEFT JOIN user u ON c.id = u.college_id;
-- Result:
┌─────────────┬──────────┐
│ college │ user │
├─────────────┼──────────┤
│ Harvard │ Alice │
│ Harvard │ Bob │
│ Stanford │ Charlie │
│ Stanford │ David │
│ MIT │ Emma │
└─────────────┴──────────┘
-- JPA automatically groups this into:
-- Harvard → [Alice, Bob]
-- Stanford → [Charlie, David]
-- MIT → [Emma]
NOTE : Entity Graph ONLY works if you have properly defined relationships among tables in your entities using JPA annotations.
Entity Graph Flow:
1. You define relationships in entities
↓
2. JPA understands: "College has a collection of Users"
↓
3. You write: @EntityGraph(attributePaths = {"users"})
↓
4. JPA knows: "users" refers to the relationship you defined
↓
5. JPA generates: LEFT JOIN user ON college.id = user.college_id
↓
6. ONE query returns all data
@EntityGraph
@EntityGraph is a Spring Data JPA annotation that tells JPA: "When loading this entity, also load these related entities in ONE query". AttributePaths parameter loads the fields needed and can load deep nested fields to based on relationships defined.
@EntityGraph(attributePaths = {"fieldName"})
List<Entity> methodName();
// syntax
@EntityGraph(attributePaths = {"fieldName"})
List<Entity> methodName();
// Load College with users AND departments
@EntityGraph(attributePaths = {"users", "departments"})
List<College> findAllWithUsersAndDepartments();
// Load College → users → profile (2 levels deep)
@EntityGraph(attributePaths = {"users", "users.profile"})
List<College> findAllWithUsersAndProfiles();
// Load College → users → profile → address (3 levels deep)
@EntityGraph(attributePaths = {"users", "users.profile", "users.profile.address"})
List<College> findAllWithFullDetails();
// With Query Conditions
@Repository
public interface CollegeRepository extends JpaRepository<College, Long> {
// Load specific city's colleges with their users
@EntityGraph(attributePaths = {"users"})
List<College> findByCity(String city);
// Load college by ID with all details
@EntityGraph(attributePaths = {"users", "departments"})
Optional<College> findById(Long id);
}
// With @Query
@Repository
public interface CollegeRepository extends JpaRepository<College, Long> {
// Entity Graph works with custom queries too
@EntityGraph(attributePaths = {"users"})
@Query("SELECT c FROM College c WHERE c.name LIKE %:name%")
List<College> searchByName(@Param("name") String name);
}
Visual: How attributePaths Works
College Entity
│
├── users (List<User>)
│ │
│ └── profile (Profile)
│ │
│ └── address (Address)
│
└── departments (List<Department>)
attributePaths = {"users"}
↓
Loads: College + users (1 level)
attributePaths = {"users", "users.profile"}
↓
Loads: College + users + profiles (2 levels)
attributePaths = {"users", "users.profile", "users.profile.address"}
↓
Loads: College + users + profiles + addresses (3 levels)
attributePaths = {"users", "departments"}
↓
Loads: College + users + departments (multiple branches)
-----------------------------------------------------------------------------------------------------------------------------
Comments
Post a Comment