首页 > Java > java教程 > 正文

JPA多对多关系与中间表映射实践指南

心靈之曲
发布: 2025-11-15 13:51:12
原创
504人浏览过

JPA多对多关系与中间表映射实践指南

本文深入探讨了在jpa中如何优雅地处理涉及中间表的复杂多对多关系。通过一个发票与产品的实际案例,我们展示了如何将一个简单的关联表(如`invoiceinfo`)重构为具有实体引用的关联实体,并利用`@manytoone`和`@onetomany`注解正确定义实体间的双向关系。文章提供了详细的代码示例和持久化操作指南,旨在帮助开发者构建健壮且易于维护的jpa实体模型。

理解多对多关系与中间表

在关系型数据库设计中,当两个实体之间存在多对多(Many-to-Many)关系时,通常会引入一个第三张表,即中间表(或称关联表、连接表),来存储这两个实体之间的关联信息。例如,一张发票(Invoice)可以包含多个产品(Product),而一个产品也可以出现在多张发票中。这种场景下,Invoice 和 Product 之间就是多对多关系,而 InvoiceInfo 表(包含 invoice_id 和 product_id)正是这种关系的中间载体。

原始的 InvoiceInfo 实体类中,productId 和 invoiceId 被定义为基本类型 long,这虽然能够反映数据库表结构,但在JPA的面向对象映射层面,它失去了实体间的直接关联性。这意味着在代码中,我们无法直接通过 InvoiceInfo 访问到它所关联的 Invoice 或 Product 实体对象,而是需要手动查询。为了充分利用JPA的强大功能并简化数据操作,我们应该将 InvoiceInfo 视为一个独立的实体,并明确定义它与 Invoice 和 Product 之间的多对一(Many-to-One)关系。

重构实体类:定义关联关系

核心思想是将 InvoiceInfo 实体类改造为真正的关联实体,使其内部包含对 Invoice 和 Product 实体的引用,而不是仅仅存储它们的外键ID。同时,在 Invoice 和 Product 实体中,也需要建立对 InvoiceInfo 的反向关联。

1. InvoiceInfo 作为关联实体

InvoiceInfo 实体将不再直接持有 productId 和 invoiceId,而是通过 @ManyToOne 注解持有 Product 和 Invoice 实体对象。@JoinColumn 注解用于指定数据库中对应的外键列名。

import javax.persistence.*;

@Entity
@Table(name = "invoice_info")
public class InvoiceInfo {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "item_id")
    private Long id;

    // 定义与 Invoice 的多对一关系
    @ManyToOne(fetch = FetchType.LAZY) // 建议使用懒加载,避免不必要的性能开销
    @JoinColumn(name = "invoice_id", nullable = false) // 对应数据库中的 invoice_id 列
    private Invoice invoice;

    // 定义与 Product 的多对一关系
    @ManyToOne(fetch = FetchType.LAZY) // 建议使用懒加载
    @JoinColumn(name = "product_id", nullable = false) // 对应数据库中的 product_id 列
    private Product product;

    // 可以在这里添加其他与此特定发票项相关的属性,例如购买数量
    // @Column(name = "quantity")
    // private int quantity;

    // 构造函数
    public InvoiceInfo() {}

    public InvoiceInfo(Invoice invoice, Product product) {
        this.invoice = invoice;
        this.product = product;
    }

    // Getters and Setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public Invoice getInvoice() { return invoice; }
    public void setInvoice(Invoice invoice) { this.invoice = invoice; }
    public Product getProduct() { return product; }
    public void setProduct(Product product) { this.product = product; }
    // public int getQuantity() { return quantity; }
    // public void setQuantity(int quantity) { this.quantity = quantity; }
}
登录后复制

2. 在 Invoice 和 Product 中建立反向关联

在 Invoice 实体中,我们需要添加一个 @OneToMany 集合来存储与该发票关联的所有 InvoiceInfo 对象。同样,Product 实体也可以选择性地添加一个 @OneToMany 集合来存储所有包含该产品的 InvoiceInfo 对象。

Invoice 实体修改:

喵记多
喵记多

喵记多 - 自带助理的 AI 笔记

喵记多 27
查看详情 喵记多
import javax.persistence.*;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;

@Entity
@Table(name = "invoice")
public class Invoice {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "invoice_id")
    private Long id;

    @Column(name = "provider_id")
    private Long providerId;

    @Column(name = "total")
    private int invoiceTotal;

    @Column(name = "date")
    private Date invoiceDate;

    // 定义与 InvoiceInfo 的一对多关系
    // mappedBy 指向 InvoiceInfo 中拥有关系的字段名 (即 private Invoice invoice;)
    // CascadeType.ALL 表示对 Invoice 的操作会级联到其关联的 InvoiceInfo 实体
    // orphanRemoval = true 表示如果 InvoiceInfo 从集合中移除,则对应的数据库记录也会被删除
    @OneToMany(mappedBy = "invoice", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
    private Set<InvoiceInfo> invoiceItems = new HashSet<>();

    // 辅助方法,用于方便地添加 InvoiceInfo 实体并维护双向关联
    public void addInvoiceItem(InvoiceInfo item) {
        invoiceItems.add(item);
        item.setInvoice(this);
    }

    // 辅助方法,用于方便地移除 InvoiceInfo 实体并维护双向关联
    public void removeInvoiceItem(InvoiceInfo item) {
        invoiceItems.remove(item);
        item.setInvoice(null);
    }

    // Getters and Setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public Long getProviderId() { return providerId; }
    public void setProviderId(Long providerId) { this.providerId = providerId; }
    public int getInvoiceTotal() { return invoiceTotal; }
    public void setInvoiceTotal(int invoiceTotal) { this.invoiceTotal = invoiceTotal; }
    public Date getInvoiceDate() { return invoiceDate; }
    public void setInvoiceDate(Date invoiceDate) { this.invoiceDate = invoiceDate; }
    public Set<InvoiceInfo> getInvoiceItems() { return invoiceItems; }
    public void setInvoiceItems(Set<InvoiceInfo> invoiceItems) { this.invoiceItems = invoiceItems; }
}
登录后复制

Product 实体修改(可选):

Product 实体通常不需要直接访问所有包含它的 InvoiceInfo 记录,但在某些分析或报告场景下可能会有用。如果需要,可以添加类似 Invoice 的 @OneToMany 关联。

import javax.persistence.*;
import java.util.HashSet;
import java.util.Set;

@Entity
@Table(name = "product")
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;

    @Column(name = "family_id")
    private long familyId;

    @Column(name = "product_name")
    private String productName;

    @Column(name = "product_category")
    private String productCategory;

    @Column(name = "product_quantity") // 这通常指库存数量
    private int productQuantity;

    // 如果需要从 Product 访问所有包含它的 InvoiceInfo 记录,可以添加此集合
    // @OneToMany(mappedBy = "product", fetch = FetchType.LAZY)
    // private Set<InvoiceInfo> productInvoiceItems = new HashSet<>();

    // Getters and Setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public long getFamilyId() { return familyId; }
    public void setFamilyId(long familyId) { this.familyId = familyId; }
    public String getProductName() { return productName; }
    public void setProductName(String productName) { this.productName = productName; }
    public String getProductCategory() { return productCategory; }
    public void setProductCategory(String productCategory) { this.productCategory = productCategory; }
    public int getProductQuantity() { return productQuantity; }
    public void setProductQuantity(int productQuantity) { this.productQuantity = productQuantity; }
    // public Set<InvoiceInfo> getProductInvoiceItems() { return productInvoiceItems; }
    // public void setProductInvoiceItems(Set<InvoiceInfo> productInvoiceItems) { this.productInvoiceItems = productInvoiceItems; }
}
登录后复制

如何进行持久化操作

通过上述实体重构,持久化一个新发票及其包含的产品信息变得更加直观。JPA会根据定义的关联关系自动处理外键的插入。

假设我们使用 Spring Data JPA 仓库接口:

// 假设您已经定义了 ProductRepository 和 InvoiceRepository
// public interface ProductRepository extends JpaRepository<Product, Long> {}
// public interface InvoiceRepository extends JpaRepository<Invoice, Long> {}

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.List;

@Service
public class InvoiceService {

    private final ProductRepository productRepository;
    private final InvoiceRepository invoiceRepository;

    public InvoiceService(ProductRepository productRepository, InvoiceRepository invoiceRepository) {
        this.productRepository = productRepository;
        this.invoiceRepository = invoiceRepository;
    }

    @Transactional
    public Invoice createNewInvoiceWithProducts(Long providerId, List<Long> productIds) {
        // 1. 创建发票实体
        Invoice newInvoice = new Invoice();
        newInvoice.setProviderId(providerId);
        newInvoice.setInvoiceDate(new Date());
        newInvoice.setInvoiceTotal(0); // 初始总价,后续可根据产品单价计算

        // 2. 遍历产品ID,创建 InvoiceInfo 关联实体
        for (Long productId : productIds) {
            Product product = productRepository.findById(productId)
                                               .orElseThrow(() -> new IllegalArgumentException("Product not found with ID: " + productId));

            InvoiceInfo invoiceItem = new InvoiceInfo();
            invoiceItem.setProduct(product);
            // 如果 InvoiceInfo 有数量字段,可以在这里设置
            // invoiceItem.setQuantity(someQuantity);

            // 通过 Invoice 的辅助方法添加 InvoiceInfo,自动维护双向关联
            newInvoice.addInvoiceItem(invoiceItem);
        }

        // 3. 持久化 Invoice 实体
        // 由于 Invoice 中设置了 cascade = CascadeType.ALL,
        // 关联的 InvoiceInfo 实体也会被自动持久化。
        return invoiceRepository.save(newInvoice);
    }

    // 示例:更新发票总价
    @Transactional
    public Invoice updateInvoiceTotal(Long invoiceId) {
        Invoice invoice = invoiceRepository.findById(invoiceId)
                                           .orElseThrow(() -> new IllegalArgumentException("Invoice not found with ID: " + invoiceId));

        int total = 0;
        for (InvoiceInfo item : invoice.getInvoiceItems()) {
            // 假设产品有单价,InvoiceInfo 有数量
            // total += item.getProduct().getPrice() * item.getQuantity();
            // 这里仅为示例,假设每个产品贡献100到总价
            total += 100;
        }
        invoice.setInvoiceTotal(total);
        return invoiceRepository.save(invoice); // 保存更新
    }
}
登录后复制

注意事项与最佳实践

  1. 双向关联的维护: 当在 @OneToMany 关系中添加或移除子实体时,务必在双向关联的两端都进行设置。例如,在 Invoice 的 addInvoiceItem 方法中,不仅要将 InvoiceInfo 添加到 invoiceItems 集合,还要调用 item.setInvoice(this) 来设置 InvoiceInfo 中的 invoice 引用。这是避免数据不一致和潜在异常的关键。
  2. 级联操作(CascadeType):
    • CascadeType.ALL 是一个强大的选项,它意味着对父实体(如 Invoice)执行的所有持久化操作(保存、更新、删除等)都会级联到子实体(如 InvoiceInfo)。这在父子实体生命周期紧密耦合时非常方便。
    • orphanRemoval = true 与 CascadeType.REMOVE 类似,但更强调“孤儿”的概念。如果一个 InvoiceInfo 实体从 Invoice 的 invoiceItems 集合中移除,并且没有其他引用,它将被视为孤儿并从数据库中删除。
    • 谨慎使用 CascadeType.ALL,尤其是在复杂的数据模型中,不恰当的级联可能导致意外的数据修改或删除。
  3. 懒加载与急加载(FetchType):
    • FetchType.LAZY(懒加载)是默认且推荐的方式,它表示在实际访问关联实体时才从数据库中加载数据。这有助于提高应用程序性能,避免加载不必要的数据。
    • FetchType.EAGER(急加载)会在加载主实体时立即加载所有关联实体。这可能导致 N+1 查询问题和内存消耗增加,应谨慎使用。
  4. **复合主

以上就是JPA多对多关系与中间表映射实践指南的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号