第 11 章 · GORM 关系与 GORM 链式 API
本章目标:为 shop-db 配置 @OneToMany / @ManyToOne 双向关系;使用 GORM 链式 API 与 Criteria API 编写类型安全查询;识别并缓解 N+1 问题;掌握 JOIN FETCH 与 gorm.ModelGraph 入门;延续 slug + is_published + price(分) 业务查询。
学时建议:6~7 小时(含 3 小时关系映射跟练)
前置:完成 go-database ch10(实体与 gorm.DB);gin-web ch05 §5.6(N+1 预习)。
11.1 场景说明:分类与商品的 1:N
电商列表页需展示「分类名 + 商品标题 + 价格」。ch10 的 Product 仅有 categoryId Long,无法 product.getCategory().getName()。本章建立对象图:
Category (1) ──────< Product (N)
id category_id FK
name title, slug, price, is_published
| 查询场景 | 需要加载 | 风险 |
|---|---|---|
| 分类页商品列表 | Product + Category | N+1 |
| 商品详情 | Product + Category | 2 次 SELECT 或可 1 次 JOIN |
| 后台分类树 | Category + products | 懒加载批量 |
11.2 @ManyToOne 与 @OneToMany 双向映射
11.2.1 Product 端(多的一方,持 FK)
gorm.Model
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "category_id", nullable = false)
private Category category;
@Column(nullable = false, length = 128)
private String title;
@Column(nullable = false, unique = true, length = 128)
private String slug;
@Column(nullable = false)
private long price; // 分
@Column(name = "is_published", nullable = false)
private boolean published = false;
@Column(nullable = false)
private int stock = 0;
protected Product() {}
public Product(String title, String slug, long priceCents, Category category) {
this.title = title;
this.slug = slug;
this.price = priceCents;
this.category = category;
}
public Category getCategory() { return category; }
public String getSlug() { return slug; }
public long getPrice() { return price; }
public boolean isPublished() { return published; }
}
11.2.2 Category 端(一的一方,反向集合)
gorm.Model
@Table(name = "categories")
public class Category {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 64)
private String name;
@Column(nullable = false, unique = true, length = 64)
private String slug;
@OneToMany(mappedBy = "category", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Product> products = new ArrayList<>();
protected Category() {}
public Category(String name, String slug) {
this.name = name;
this.slug = slug;
}
public void addProduct(Product product) {
products.add(product);
product.setCategory(this);
}
public List<Product> getProducts() { return products; }
}
Product.setCategory(Category) 为 package-private,仅 addProduct 调用,防止单向设置导致 FK 不一致。
| 注解 | 说明 |
|---|---|
@ManyToOne | 多 Product 对一 Category |
@JoinColumn(name="category_id") | FK 列 |
FetchType.LAZY | 默认懒加载 category |
@OneToMany(mappedBy="category") | 反向关系,不建额外表 |
orphanRemoval=true | 从集合移除则 DELETE 商品 |
11.2.3 与 gin-web ch05 对照
| gin-web ch05 | shop-db ch11 | 备注 |
|---|---|---|
@ManyToOne Category | 同左 | FK 一致 |
@OneToMany(mappedBy="category") | 同左 | 需 addProduct 维护双向 |
| 无 slug 在 Category | Category.slug | shop-db 扩展 |
11.3 级联(Cascade)与 orphanRemoval
@OneToMany(mappedBy = "category", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Product> products = new ArrayList<>();
| CascadeType | 行为 |
|---|---|
PERSIST | persist Category 时级联 persist Product |
MERGE | merge 时级联 |
REMOVE | remove Category 时级联 remove Product(慎用) |
ALL | 以上全部 |
shop-db 建议:
- 删分类:DB 层
ON DELETE RESTRICT,应用层禁止remove(category)若有商品。 orphanRemoval:仅从集合remove(product)时删商品行,符合「从分类移除即删 SKU」后台操作(需权限控制)。
11.4 GORM 链式 API 基础
GORM 链式 API 面向实体,非表:
// 按 slug 查上架商品
Product p = em.createQuery("""
SELECT p FROM Product p
WHERE p.slug = :slug AND p.published = true
""", Product.class)
.setParameter("slug", "wireless-mouse")
.getSingleResult();
// 分类下已上架商品,按价升序
List<Product> list = em.createQuery("""
SELECT p FROM Product p
JOIN p.category c
WHERE c.slug = :catSlug AND p.published = true
ORDER BY p.price ASC
""", Product.class)
.setParameter("catSlug", "digital")
.getResultList();
| GORM 链式 API 片段 | SQL 近似 |
|---|---|
FROM Product p | FROM products p |
JOIN p.category c | JOIN categories c ON ... |
p.published = true | is_published = 1 |
p.price | price(分,整数比较) |
11.4.1 分页
List<Product> page = em.createQuery("""
SELECT p FROM Product p WHERE p.published = true ORDER BY p.createdAt DESC
""", Product.class)
.setFirstResult(0)
.setMaxResults(20)
.getResultList();
11.4.3 GORM 链式 API 速查
| 业务需求 | GORM 链式 API 片段 |
|---|---|
| 按 slug 查上架商品 | WHERE p.slug = :slug AND p.published = true |
| 分类下商品列表 | JOIN p.category c WHERE c.slug = :catSlug |
| 统计上架数 | SELECT COUNT(p) FROM Product p WHERE p.published = true |
| 最低价(分) | SELECT MIN(p.price) FROM Product p WHERE p.published = true |
分页用 setFirstResult / setMaxResults;聚合返回 Long.class。
11.5 JOIN FETCH 解决 N+1
11.5.1 N+1 现象
List<Product> products = em.createQuery(
"SELECT p FROM Product p WHERE p.published = true", Product.class)
.getResultList();