Spring Data JPA Data Persistence
JPA makes the mapping between objects and relational databases declarative—you write Entities, define Repositories, and SQL is automatically generated.
1. What You'll Learn
- JPA Entity Definitions:
@Entity/@Id/@GeneratedValue/@ColumnAnnotations - Repository Interface:
CrudRepository/JpaRepository/ Custom Query Methods @OneToMany/@ManyToOneEntity Relationship Mapping (Order ↔ OrderItem)@QueryJPQL and Native SQL Queries- Database Initialization:
schema.sql/data.sqland Hibernateddl-autoStrategies
2. A True Story About Data Persistence
(1) Pain Point: In-Memory Data Is Lost Upon Restart
Alice previously used an in-memory map to store product and order data for OrderFlow. Every time the application restarted, all the data was lost, forcing her to manually recreate the products during testing. Even more troublesome was that the map couldn't support complex queries (such as searching for orders within a date range), so a real database had to be used in the production environment.
(2) The Solution Using Spring Data JPA
Spring Data JPA makes persistence a matter of "defining interfaces":
public interface ProductRepository extends JpaRepository<Product, Long> {
List<Product> findByNameContaining(String keyword);
}
Without an implementation class, Spring Data automatically generates SQL.
(3) Revenue
After Alice introduced JPA, OrderFlow data is persisted to MySQL and is retained after a restart. Complex queries are implemented using method naming conventions or @Query, and the Repository interface contains no implementation code.
3. Defining JPA Entities
(1) Core Entity Annotations
| Comment | Purpose | Example |
|---|---|---|
@Entity |
Marked as a JPA entity | @Entity public class Product |
@Table |
Specify table name | @Table(name = "products") |
@Id |
Mark Primary Key | @Id private Long id; |
@GeneratedValue |
Primary Key Generation Strategy | @GeneratedValue(strategy = IDENTITY) |
@Column |
Column Mapping | @Column(nullable = false, length = 200) |
@CreationTimestamp |
Auto-fill creation time | @CreationTimestamp private Instant createdAt; |
(1) ▶ Example: Product Entity
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 200)
private String name;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal price;
@Column(nullable = false)
private Integer stock;
@CreationTimestamp
private Instant createdAt;
// Default constructor required by JPA
protected Product() {}
public Product(String name, BigDecimal price, Integer stock) {
this.name = name;
this.price = price;
this.stock = stock;
}
// Getters and setters omitted for brevity
}
Output:
// Execution Successful
ddl-auto Value |
Behavior | Applicable Environment |
|---|---|---|
create-drop |
Enable table creation, disable table deletion | Test |
create |
Rebuild table on every startup | Development |
update |
Add only, do not delete; modify columns | Development |
validate |
Verify only, do not modify | Production |
none |
Take no action | Production (Manual Management) |
validate or none; using update may result in data loss.
4. Repository Interface
(1) Repository Inheritance Hierarchy
graph LR
A[Repository] --> B[CrudRepository]
B --> C[ListCrudRepository]
B --> D[PagingAndSortingRepository]
D --> E[JpaRepository]
C --> E
| Interface | Core Methods | Use Cases |
|---|---|---|
CrudRepository |
save / findById / findAll / delete | Basic CRUD |
ListCrudRepository |
findAll returns a List | Avoid Iterable conversion |
PagingAndSortingRepository |
findAll(Pageable) | Pagination and Sorting |
JpaRepository |
All of the above + flush / saveAndFlush | Most commonly used, recommended |
(1) ▶ Example: ProductRepository
public interface ProductRepository extends JpaRepository<Product, Long> {
// Derived query by method name
List<Product> findByNameContaining(String keyword);
List<Product> findByStockLessThan(Integer threshold);
List<Product> findByPriceBetween(BigDecimal min, BigDecimal max);
// Custom JPQL
@Query("SELECT p FROM Product p WHERE p.stock = 0")
List<Product> findOutOfStockProducts();
// Native SQL
@Query(value = "SELECT * FROM products WHERE price > :minPrice ORDER BY price DESC",
nativeQuery = true)
List<Product> findExpensiveProducts(@Param("minPrice") BigDecimal minPrice);
}
Output:
// Execution Successful
(2) Conventions for Method Names
| Keyword | Example | Generated SQL |
|---|---|---|
findBy |
findByName |
WHERE name = ? |
Containing |
findByNameContaining |
WHERE name LIKE %?% |
Between |
findByPriceBetween |
WHERE price BETWEEN ? AND ? |
LessThan |
findByStockLessThan |
WHERE stock < ? |
OrderBy |
findByPriceOrderByStockDesc |
ORDER BY stock DESC |
And / Or |
findByNameAndStock |
WHERE name = ? AND stock = ? |
5. Entity-Relationship Mapping
(1) Order ↔ OrderItem: One-to-Many Relationship
erDiagram
ORDER ||--o{ ORDER_ITEM : contains
PRODUCT ||--o{ ORDER_ITEM : included_in
ORDER {
bigint id PK
varchar status
decimal total_amount
timestamp created_at
}
ORDER_ITEM {
bigint id PK
bigint order_id FK
bigint product_id FK
int quantity
decimal unit_price
}
PRODUCT {
bigint id PK
varchar name
decimal price
int stock
}
(1) ▶ Example: Order and OrderItem Entities
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 20)
private String status;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal totalAmount;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> items = new ArrayList<>();
@CreationTimestamp
private Instant createdAt;
protected Order() {}
public void addItem(OrderItem item) {
items.add(item);
item.setOrder(this);
}
}
@Entity
@Table(name = "order_items")
public class OrderItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id", nullable = false)
private Order order;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id", nullable = false)
private Product product;
@Column(nullable = false)
private Integer quantity;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal unitPrice;
protected OrderItem() {}
}
Output:
// Execution Successful
| Cascade Type | Meaning | Recommended Usage |
|---|---|---|
ALL |
All operations are cascaded | Used only for strong dependencies |
PERSIST |
persist cascade | Common |
MERGE |
merge cascade | Common |
REMOVE |
delete cascade | Use with caution |
| Fetch Type | Behavior | Use Cases |
|---|---|---|
LAZY |
Query only on access | Default recommendation to avoid N+1 |
EAGER |
Load Now | @ManyToOne Default Value |
6. @Query JPQL vs. Native SQL
(1) ▶ Example: JPQL and Native SQL Queries
public interface OrderRepository extends JpaRepository<Order, Long> {
// JPQL: find orders by status with item count
@Query("SELECT o FROM Order o WHERE o.status = :status")
List<Order> findByStatus(@Param("status") String status);
// JPQL: join fetch to avoid N+1
@Query("SELECT DISTINCT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
Optional<Order> findByIdWithItems(@Param("id") Long id);
// Native SQL: order statistics
@Query(value = """
SELECT o.status, COUNT(*) as cnt, SUM(o.total_amount) as total
FROM orders o
GROUP BY o.status
""", nativeQuery = true)
List<Object[]> getOrderStatistics();
}
Output:
// Execution Successful
| Dimension | JPQL | Native SQL |
|---|---|---|
| Syntax | Entity- and Field-Based | Table- and Column-Based |
| Database Migration | Portable | Not Portable |
| Features | Limited (does not support UNION, etc.) | Full |
| Return Type | Entity / DTO | Object[] / DTO |
7. Database Initialization
(1) ▶ Example: schema.sql and data.sql
-- src/main/resources/schema.sql
CREATE TABLE IF NOT EXISTS products (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(200) NOT NULL,
price DECIMAL(10,2) NOT NULL,
stock INT NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS orders (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
total_amount DECIMAL(10,2) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Output:
CREATE TABLE
-- src/main/resources/data.sql
INSERT INTO products (name, price, stock) VALUES
('Laptop Pro', 1299.99, 50),
('Wireless Mouse', 29.99, 200),
('USB-C Hub', 49.99, 100);
| Initialization Method | Configuration | Use Cases |
|---|---|---|
Hibernate ddl-auto |
spring.jpa.hibernate.ddl-auto=update |
Development |
schema.sql + data.sql |
spring.sql.init.mode=always |
Requires precise control of DDL |
| Flyway | spring.flyway.enabled=true |
Production Migration |
| Liquibase | spring.liquibase.enabled=true |
Production Migration |
8. Comprehensive Example: Complete Implementation of OrderFlow JPA Persistence
// Product.java
@Entity
@Table(name = "products")
public class Product {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 200)
private String name;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal price;
@Column(nullable = false)
private Integer stock;
@CreationTimestamp
private Instant createdAt;
protected Product() {}
public Product(String name, BigDecimal price, Integer stock) {
this.name = name; this.price = price; this.stock = stock;
}
// getters/setters
}
// Order.java
@Entity
@Table(name = "orders")
public class Order {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 20)
private String status = "PENDING";
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal totalAmount = BigDecimal.ZERO;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> items = new ArrayList<>();
@CreationTimestamp
private Instant createdAt;
protected Order() {}
public void addItem(Product product, int quantity) {
OrderItem item = new OrderItem(this, product, quantity, product.getPrice());
items.add(item);
totalAmount = totalAmount.add(product.getPrice().multiply(BigDecimal.valueOf(quantity)));
}
// getters/setters
}
// OrderItem.java
@Entity
@Table(name = "order_items")
public class OrderItem {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id")
private Order order;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id")
private Product product;
@Column(nullable = false)
private Integer quantity;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal unitPrice;
protected OrderItem() {}
public OrderItem(Order order, Product product, Integer quantity, BigDecimal unitPrice) {
this.order = order; this.product = product;
this.quantity = quantity; this.unitPrice = unitPrice;
}
// getters/setters
}
// ProductRepository.java
public interface ProductRepository extends JpaRepository<Product, Long> {
List<Product> findByNameContaining(String keyword);
List<Product> findByStockLessThan(Integer threshold);
}
❓ FAQ
protected to prevent accidental misuse from outside. Since records do not have a no-argument constructor, they cannot be used directly as JPA entities.@Transactional Keep the session open; 2) JOIN FETCH Load all at once; 3) @EntityGraph Define a loading schedule.ddl-auto=update safe?update option only inserts data and does not delete it, which may leave behind obsolete columns. Additionally, changing column types may result in data loss. In a production environment, you must use validate along with Flyway or Liquibase to manage database migrations.@Query("JOIN FETCH") Load related data all at once; 2) @EntityGraph Declarative load plan; 3) @BatchSize Batch loading. This is the most critical aspect of JPA performance optimization.ddl-auto=none.📖 Summary
- JPA Entities use annotations to map objects to relational tables;
@Id+@GeneratedValuemanage the primary key JpaRepositoryProvides full CRUD functionality + pagination + sorting; automatically generates queries based on method naming conventions@OneToMany/@ManyToOneMap entity relationships; note the cascade and fetch strategies@QueryJPQL is suitable for entity queries, while native SQL is suitable for complex statistics- Use
validate+ migration tools (Flyway/Liquibase) in the production environment; do not useddl-auto=update
📝 Exercises
-
Basic Exercise (Difficulty: ⭐): Configure an H2 data source for OrderFlow, create the Product Entity and ProductRepository, implement a CRUD REST API for products, and persist data to H2.
-
Advanced Problem (Difficulty ⭐⭐): Implement the one-to-many relationship between
OrderandOrderItem, useJOIN FETCHto solve the N+1 problem, and write a Repository method to query orders by date range. -
Challenge (Difficulty: ⭐⭐⭐): Switch to the MySQL data source, use Flyway to manage database migration scripts, and create the migration files V1__init_schema.sql and V2__add_order_status_index.sql.



