Spring Boot: Spring Data JPA 数据持久化

最后更新:2026-08-26

JPA 让对象与关系数据库之间的映射变成声明式——写 Entity,定义 Repository,SQL 自动生成。

1. 你将学到


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 不做任何操作 生产(手动管理)
⚠️ 注意: 生产环境必须用 validatenoneupdate 可能导致数据丢失。


4. Repository 接口

(1) Repository 继承体系

100%
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 一对多关系

100%
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

📖 小节


📝 作业

  1. 基础题(难度⭐):为 OrderFlow 配置 H2 数据源,创建 Product Entity 和 ProductRepository,实现商品 CRUD REST API,数据持久化到 H2。

  2. 进阶题(难度⭐⭐):实现 Order ↔ OrderItem 一对多关系,使用 JOIN FETCH 解决 N+1 问题,编写按日期范围查询订单的 Repository 方法。

  3. 挑战题(难度⭐⭐⭐):切换到 MySQL 数据源,使用 Flyway 管理数据库迁移脚本,创建 V1__init_schema.sql 和 V2__add_order_status_index.sql 迁移文件。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏