第 5 章 · Spring Data JPA 与实体建模
本章目标:使用 @Entity 定义商品与分类;编写 JpaRepository 接口实现 CRUD;掌握 一对多 / 多对一 关系映射与懒加载注意点;配置 H2/MySQL 数据源;了解 Flyway 数据库版本迁移;将 ch02~ch04 的内存数据替换为持久化层;对照 shop-demo Django ORM 说明异同。
学时建议:6~7 小时(含 2.5 小时跟练)
前置:完成 spring-boot-web ch04;java-dev ch11(JDBC/SQL);django-web ch05 Model 章节更佳。
5.1 场景说明:从内存 List 到数据库
ch02 的 InMemoryProductCatalogService 重启即丢失。运营后台需要持久化商品与分类,与 shop-demo 的 Product、Category 模型对齐(教学虚构字段)。
| 层次 | shop-demo | shop-spring-demo |
|---|---|---|
| 模型 | models.Product | @Entity Product |
| 查询 | Product.objects.filter(...) | productRepository.findBy... |
| 迁移 | makemigrations / migrate | Flyway |
| 数据库 | SQLite / PostgreSQL | H2 开发 / MySQL 联调 |
API 仍经虚构 https://api.example.com 对外;数据库连接串仅用本地或占位 hostname,勿写真实生产凭证。
5.2 依赖与数据源
5.2.1 pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
5.2.2 application-dev.yml
spring:
datasource:
url: jdbc:h2:mem:shop_spring;DB_CLOSE_DELAY=-1;MODE=MySQL
driver-class-name: org.h2.Driver
username: sa
password:
h2:
console:
enabled: true
path: /h2-console
jpa:
hibernate:
ddl-auto: validate # 配合 Flyway,勿用 create-drop 上生产
show-sql: true
properties:
hibernate:
format_sql: true
ddl-auto | 含义 | 环境 |
|---|---|---|
create-drop | 启动建表、关闭删表 | 仅本地实验 |
update | 自动改表(不可靠) | 不推荐生产 |
validate | 只校验与 DB 一致 | 配合迁移工具 |
none | 不管理 schema | 完全交给 Flyway |
Django:migrate 由迁移文件驱动;Spring 推荐 validate + Flyway 而非 ddl-auto=update。
5.3 实体 Category 与 Product
5.3.1 Category
package com.example.shop.catalog.entity;
import jakarta.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "categories")
public class Category {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 64)
private String name;
@OneToMany(mappedBy = "category", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Product> products = new ArrayList<>();
protected Category() {}
public Category(String name) {
this.name = name;
}
public Long getId() { return id; }
public String getName() { return name; }
public List<Product> getProducts() { return products; }
}
5.3.2 Product
@Entity
@Table(name = "products", indexes = {
@Index(name = "idx_product_sku", columnList = "sku", unique = true)
})
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 32)
private String sku;
@Column(nullable = false, length = 128)
private String name;
@Column(nullable = false, precision = 12, scale = 2)
private BigDecimal price;
@Column(nullable = false)
private int stock = 0;
@Column(nullable = false)
private boolean active = true;
@Column(nullable = false, updatable = false)
private Instant createdAt = Instant.now();
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "category_id")
private Category category;
protected Product() {}
public Product(String sku, String name, BigDecimal price, Category category) {
this.sku = sku;
this.name = name;
this.price = price;
this.category = category;
}
}
5.3.3 与 Django Model 对照
| JPA | Django |
|---|---|
@Entity | models.Model |
@Id @GeneratedValue | AutoField primary key |
@Column | CharField / DecimalField |
@ManyToOne | ForeignKey |
@OneToMany(mappedBy=) | related_name 反向 |
FetchType.LAZY | 默认 lazy FK |
@Table(indexes=) | Meta.indexes |
5.4 Repository 接口
public interface ProductRepository extends JpaRepository<Product, Long> {
Optional<Product> findBySku(String sku);
Page<Product> findByActiveTrueAndNameContainingIgnoreCase(String name, Pageable pageable);
Page<Product> findByActiveTrue(Pageable pageable);
}
public interface CategoryRepository extends JpaRepository<Category, Long> {
Optional<Category> findByName(String name);
}
无需实现类——Spring Data 在运行时生成代理。
| Django ORM | Spring Data JPA |
|---|---|
Product.objects.get(sku=...) | findBySku |
filter(is_active=True) | findByActiveTrue... |
[:20] 切片 | Pageable(ch07) |
Q() 复杂查询 | @Query JPQL / Specification |
5.4.1 自定义 @Query 示例
@Query("SELECT p FROM Product p JOIN FETCH p.category WHERE p.active = true")
List<Product> findActiveWithCategory();
JOIN FETCH 一次加载关联分类,避免列表页 N+1。