下载工作台
Java 数据库实战

Hibernate 关系与 JPQL

试读上半部分 · 解锁后可读全文

第 11 章 · Hibernate 关系与 JPQL

本章目标:为 shop-db 配置 @OneToMany / @ManyToOne 双向关系;使用 JPQLCriteria API 编写类型安全查询;识别并缓解 N+1 问题;掌握 JOIN FETCH@EntityGraph 入门;延续 slug + is_published + price(分) 业务查询。

学时建议:6~7 小时(含 3 小时关系映射跟练)

前置:完成 java-database ch10(实体与 EntityManager);spring-boot-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 + CategoryN+1
商品详情Product + Category2 次 SELECT 或可 1 次 JOIN
后台分类树Category + products懒加载批量

11.2 @ManyToOne 与 @OneToMany 双向映射

11.2.1 Product 端(多的一方,持 FK)

@Entity
@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 端(一的一方,反向集合)

@Entity
@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 与 spring-boot-web ch05 对照

spring-boot-web ch05shop-db ch11备注
@ManyToOne Category同左FK 一致
@OneToMany(mappedBy="category")同左addProduct 维护双向
无 slug 在 CategoryCategory.slugshop-db 扩展

11.3 级联(Cascade)与 orphanRemoval

@OneToMany(mappedBy = "category", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Product> products = new ArrayList<>();
CascadeType行为
PERSISTpersist Category 时级联 persist Product
MERGEmerge 时级联
REMOVEremove Category 时级联 remove Product(慎用)
ALL以上全部

shop-db 建议

  • 删分类:DB 层 ON DELETE RESTRICT,应用层禁止 remove(category) 若有商品。
  • orphanRemoval:仅从集合 remove(product) 时删商品行,符合「从分类移除即删 SKU」后台操作(需权限控制)。

11.4 JPQL 基础

JPQL 面向实体,非表:

// 按 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();
JPQL 片段SQL 近似
FROM Product pFROM products p
JOIN p.category cJOIN categories c ON ...
p.published = trueis_published = 1
p.priceprice(分,整数比较)

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 JPQL 速查

业务需求JPQL 片段
按 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

以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。