0

0

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

心靈之曲

心靈之曲

发布时间:2025-11-15 13:51:12

|

532人浏览过

|

来源于php中文网

原创

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 实体修改:

ProcessOn
ProcessOn

免费在线流程图思维导图,专业强大的作图工具,支持多人实时在线协作

下载
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 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 getInvoiceItems() { return invoiceItems; }
    public void setInvoiceItems(Set 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 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 getProductInvoiceItems() { return productInvoiceItems; }
    // public void setProductInvoiceItems(Set productInvoiceItems) { this.productInvoiceItems = productInvoiceItems; }
}

如何进行持久化操作

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

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

// 假设您已经定义了 ProductRepository 和 InvoiceRepository
// public interface ProductRepository extends JpaRepository {}
// public interface InvoiceRepository extends JpaRepository {}

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 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. **复合主

相关专题

更多
spring框架介绍
spring框架介绍

本专题整合了spring框架相关内容,想了解更多详细内容,请阅读专题下面的文章。

101

2025.08.06

go语言 面向对象
go语言 面向对象

本专题整合了go语言面向对象相关内容,阅读专题下面的文章了解更多详细内容。

54

2025.09.05

java面向对象
java面向对象

本专题整合了java面向对象相关内容,阅读专题下面的文章了解更多详细内容。

47

2025.11.27

硬盘接口类型介绍
硬盘接口类型介绍

硬盘接口类型有IDE、SATA、SCSI、Fibre Channel、USB、eSATA、mSATA、PCIe等等。详细介绍:1、IDE接口是一种并行接口,主要用于连接硬盘和光驱等设备,它主要有两种类型:ATA和ATAPI,IDE接口已经逐渐被SATA接口;2、SATA接口是一种串行接口,相较于IDE接口,它具有更高的传输速度、更低的功耗和更小的体积;3、SCSI接口等等。

995

2023.10.19

PHP接口编写教程
PHP接口编写教程

本专题整合了PHP接口编写教程,阅读专题下面的文章了解更多详细内容。

53

2025.10.17

php8.4实现接口限流的教程
php8.4实现接口限流的教程

PHP8.4本身不内置限流功能,需借助Redis(令牌桶)或Swoole(漏桶)实现;文件锁因I/O瓶颈、无跨机共享、秒级精度等缺陷不适用高并发场景。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

248

2025.12.29

数据库三范式
数据库三范式

数据库三范式是一种设计规范,用于规范化关系型数据库中的数据结构,它通过消除冗余数据、提高数据库性能和数据一致性,提供了一种有效的数据库设计方法。本专题提供数据库三范式相关的文章、下载和课程。

338

2023.06.29

如何删除数据库
如何删除数据库

删除数据库是指在MySQL中完全移除一个数据库及其所包含的所有数据和结构,作用包括:1、释放存储空间;2、确保数据的安全性;3、提高数据库的整体性能,加速查询和操作的执行速度。尽管删除数据库具有一些好处,但在执行任何删除操作之前,务必谨慎操作,并备份重要的数据。删除数据库将永久性地删除所有相关数据和结构,无法回滚。

2068

2023.08.14

php源码安装教程大全
php源码安装教程大全

本专题整合了php源码安装教程,阅读专题下面的文章了解更多详细内容。

189

2025.12.31

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 2.2万人学习

C# 教程
C# 教程

共94课时 | 5.9万人学习

Java 教程
Java 教程

共578课时 | 41.1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

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