Spring Boot: Spring Data JPA 数据持久化
最后更新:2026-08-26
JPA 让对象与关系数据库之间的映射变成声明式——写 Entity,定义 Repository,SQL 自动生成。
1. 你将学到
- JPA Entity 定义:
@Entity/@Id/@GeneratedValue/@Column注解 - Repository 接口:
CrudRepository/JpaRepository/ 自定义查询方法 @OneToMany/@ManyToOne实体关系映射(Order ↔ OrderItem)@QueryJPQL 与原生 SQL 查询- 数据库初始化:
schema.sql/data.sql与 Hibernateddl-auto策略
2. 一个数据持久化的真实故事
(1) 痛点:内存数据重启就丢失
Alice 之前用内存 Map 存储 OrderFlow 的商品和订单数据。每次应用重启,所有数据都消失了,测试时不得不手动重新创建商品。更麻烦的是,Map 无法支持复杂查询(如按日期范围查找订单),生产环境必须用真正的数据库。
(2) Spring Data JPA 的解法
Spring Data JPA 让持久化变成"定义接口":
JAVA
public interface ProductRepository extends JpaRepository<Product, Long> {
List<Product> findByNameContaining(String keyword);
}
没有实现类,Spring Data 自动生成 SQL。
(3) 收益
Alice 引入 JPA 后,OrderFlow 数据持久化到 MySQL,重启不丢失。复杂查询用方法名约定或 @Query 实现,Repository 接口零实现代码。
3. JPA Entity 定义
(1) 核心 Entity 注解
| 注解 | 用途 | 示例 |
|---|---|---|
@Entity |
标记为 JPA 实体 | @Entity public class Product |
@Table |
指定表名 | @Table(name = "products") |
@Id |
标记主键 | @Id private Long id; |
@GeneratedValue |
主键生成策略 | @GeneratedValue(strategy = IDENTITY) |
@Column |
列映射 | @Column(nullable = false, length = 200) |
@CreationTimestamp |
自动填充创建时间 | @CreationTimestamp private Instant createdAt; |
▶ 示例: Product Entity
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;
// 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
}
输出:
TEXT
📖 仅展示
// 执行成功
ddl-auto 值 |
行为 | 适用环境 |
|---|---|---|
create-drop |
启动建表,关闭删表 | 测试 |
create |
每次启动重建表 | 开发 |
update |
只增不删,修改列 | 开发 |
validate |
只校验不修改 | 生产 |
none |
不做任何操作 | 生产(手动管理) |
⚠️ 注意: 生产环境必须用
validate 或 none,update 可能导致数据丢失。
4. Repository 接口
(1) Repository 继承体系
graph LR
A[Repository] --> B[CrudRepository]
B --> C[ListCrudRepository]
B --> D[PagingAndSortingRepository]
D --> E[JpaRepository]
C --> E
| 接口 | 核心方法 | 适用场景 |
|---|---|---|
CrudRepository |
save / findById / findAll / delete | 基本 CRUD |
ListCrudRepository |
findAll 返回 List | 避免 Iterable 转换 |
PagingAndSortingRepository |
findAll(Pageable) | 分页排序 |
JpaRepository |
以上全部 + flush / saveAndFlush | 最常用,推荐 |
▶ 示例: ProductRepository
JAVA
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);
}
输出:
TEXT
📖 仅展示
// 执行成功
(2) 方法名查询约定
| 关键字 | 示例 | 生成的 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. 实体关系映射
(1) Order ↔ OrderItem 一对多关系
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
}
▶ 示例: Order 和 OrderItem Entity
JAVA
@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() {}
}
输出:
TEXT
📖 仅展示
// 执行成功
| Cascade 类型 | 含义 | 使用建议 |
|---|---|---|
ALL |
所有操作级联 | 仅用于强从属关系 |
PERSIST |
persist 级联 | 常用 |
MERGE |
merge 级联 | 常用 |
REMOVE |
delete 级联 | 谨慎使用 |
| Fetch 类型 | 行为 | 适用场景 |
|---|---|---|
LAZY |
访问时才查询 | 默认推荐,避免 N+1 |
EAGER |
立即加载 | @ManyToOne 默认值 |
6. @Query JPQL 与原生 SQL
▶ 示例: JPQL 和原生 SQL 查询
JAVA
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();
}
输出:
TEXT
📖 仅展示
// 执行成功
| 维度 | JPQL | 原生 SQL |
|---|---|---|
| 语法 | 基于实体和字段 | 基于表和列 |
| 数据库移植 | 可移植 | 不可移植 |
| 功能 | 有限(不支持 UNION 等) | 完整 |
| 返回类型 | 实体 / DTO | Object[] / DTO |
7. 数据库初始化
▶ 示例: schema.sql 与 data.sql
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
);
输出:
TEXT
📖 仅展示
CREATE TABLE
SQL
-- 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);
| 初始化方式 | 配置 | 适用场景 |
|---|---|---|
Hibernate ddl-auto |
spring.jpa.hibernate.ddl-auto=update |
开发 |
schema.sql + data.sql |
spring.sql.init.mode=always |
需要精确控制 DDL |
| Flyway | spring.flyway.enabled=true |
生产迁移 |
| Liquibase | spring.liquibase.enabled=true |
生产迁移 |
8. 综合示例:OrderFlow JPA 持久化完整实现
JAVA
// 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);
}
❓ 常见问题
Q JPA Entity 为什么必须有无参构造器?
A JPA 使用反射创建实体实例,要求有无参构造器。可以用
protected 修饰,防止外部误用。record 没有无参构造器,不能直接用作 JPA Entity。Q LazyInitializationException 怎么解决?
A 原因是在 Session 关闭后访问懒加载属性。解决方案:1)
@Transactional 保持 Session 打开;2)JOIN FETCH 一次性加载;3)@EntityGraph 定义加载计划。Q CrudRepository 和 JpaRepository 选哪个?
A 推荐 JpaRepository,它继承自所有子接口,提供最完整的方法集(包括分页、排序、批量操作),是实际开发的标准选择。
Q ddl-auto=update 安全吗?
A 不安全。update 只增不删,可能遗留废弃列,且修改列类型可能丢数据。生产环境必须用 validate + Flyway/Liquibase 管理数据库迁移。
Q 如何处理 N+1 查询问题?
A 1)
@Query("JOIN FETCH") 一次性加载关联数据;2)@EntityGraph 声明式加载计划;3)@BatchSize 批量加载。这是 JPA 性能优化最重要的一环。Q schema.sql 和 Hibernate ddl-auto 可以同时用吗?
A 不建议同时使用,可能冲突。如果用 Hibernate 管理 DDL,就不需要 schema.sql;如果用 schema.sql,设置
ddl-auto=none。📖 小节
- JPA Entity 用注解映射对象到关系表,
@Id+@GeneratedValue管理主键 JpaRepository提供完整 CRUD + 分页 + 排序,方法名约定自动生成查询@OneToMany/@ManyToOne映射实体关系,注意 cascade 和 fetch 策略@QueryJPQL 适合实体查询,原生 SQL 适合复杂统计- 生产环境用
validate+ 迁移工具(Flyway/Liquibase),不用ddl-auto=update
📝 作业
-
基础题(难度⭐):为 OrderFlow 配置 H2 数据源,创建 Product Entity 和 ProductRepository,实现商品 CRUD REST API,数据持久化到 H2。
-
进阶题(难度⭐⭐):实现 Order ↔ OrderItem 一对多关系,使用
JOIN FETCH解决 N+1 问题,编写按日期范围查询订单的 Repository 方法。 -
挑战题(难度⭐⭐⭐):切换到 MySQL 数据源,使用 Flyway 管理数据库迁移脚本,创建 V1__init_schema.sql 和 V2__add_order_status_index.sql 迁移文件。