JPA多对多映射与中间表详解
小伙伴们对文章编程感兴趣吗?是否正在学习相关知识点?如果是,那么本文《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 实体修改:
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); // 保存更新
}
}注意事项与最佳实践
- 双向关联的维护: 当在 @OneToMany 关系中添加或移除子实体时,务必在双向关联的两端都进行设置。例如,在 Invoice 的 addInvoiceItem 方法中,不仅要将 InvoiceInfo 添加到 invoiceItems 集合,还要调用 item.setInvoice(this) 来设置 InvoiceInfo 中的 invoice 引用。这是避免数据不一致和潜在异常的关键。
- 级联操作(CascadeType):
- CascadeType.ALL 是一个强大的选项,它意味着对父实体(如 Invoice)执行的所有持久化操作(保存、更新、删除等)都会级联到子实体(如 InvoiceInfo)。这在父子实体生命周期紧密耦合时非常方便。
- orphanRemoval = true 与 CascadeType.REMOVE 类似,但更强调“孤儿”的概念。如果一个 InvoiceInfo 实体从 Invoice 的 invoiceItems 集合中移除,并且没有其他引用,它将被视为孤儿并从数据库中删除。
- 谨慎使用 CascadeType.ALL,尤其是在复杂的数据模型中,不恰当的级联可能导致意外的数据修改或删除。
- 懒加载与急加载(FetchType):
- FetchType.LAZY(懒加载)是默认且推荐的方式,它表示在实际访问关联实体时才从数据库中加载数据。这有助于提高应用程序性能,避免加载不必要的数据。
- FetchType.EAGER(急加载)会在加载主实体时立即加载所有关联实体。这可能导致 N+1 查询问题和内存消耗增加,应谨慎使用。
- **复合主
文中关于的知识介绍,希望对你的学习有所帮助!若是受益匪浅,那就动动鼠标收藏这篇《JPA多对多映射与中间表详解》文章吧,也可关注golang学习网公众号了解相关技术文章。
Java压缩解压ZIP全攻略教程
- 上一篇
- Java压缩解压ZIP全攻略教程
- 下一篇
- 淘宝双11红包雨怎么领?
-
- 文章 · java教程 | 20分钟前 | 环境变量 jdk java-version javac-version Java环境验证
- Java安装后怎么检查环境是否配置成功
- 402浏览 收藏
-
- 文章 · java教程 | 23分钟前 | 缓冲区 JavaNIO BufferOverflowException BufferUnderflowException flip()
- Java缓冲异常处理方法解析
- 351浏览 收藏
-
- 文章 · java教程 | 24分钟前 |
- Java对象序列化保存方法详解
- 355浏览 收藏
-
- 文章 · java教程 | 26分钟前 |
- 读写锁特性解析与实际应用
- 264浏览 收藏
-
- 文章 · java教程 | 29分钟前 |
- JavaSemaphore限流实现与高并发优化
- 226浏览 收藏
-
- 文章 · java教程 | 44分钟前 |
- 数据表格列冻结问题及解决方法
- 498浏览 收藏
-
- 文章 · java教程 | 59分钟前 |
- 原子类底层原理深度解析
- 254浏览 收藏
-
- 文章 · java教程 | 1小时前 |
- SpringBoot并发数据隔离与共享管理技巧
- 378浏览 收藏
-
- 文章 · java教程 | 1小时前 | java split() 字符串分割 StringTokenizer 遗留类
- StringTokenizer使用方法与解析技巧详解
- 332浏览 收藏
-
- 文章 · java教程 | 1小时前 |
- Java定时任务对比:Timer与ScheduledExecutorService详解
- 411浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- ChatExcel酷表
- ChatExcel酷表是由北京大学团队打造的Excel聊天机器人,用自然语言操控表格,简化数据处理,告别繁琐操作,提升工作效率!适用于学生、上班族及政府人员。
- 3178次使用
-
- Any绘本
- 探索Any绘本(anypicturebook.com/zh),一款开源免费的AI绘本创作工具,基于Google Gemini与Flux AI模型,让您轻松创作个性化绘本。适用于家庭、教育、创作等多种场景,零门槛,高自由度,技术透明,本地可控。
- 3389次使用
-
- 可赞AI
- 可赞AI,AI驱动的办公可视化智能工具,助您轻松实现文本与可视化元素高效转化。无论是智能文档生成、多格式文本解析,还是一键生成专业图表、脑图、知识卡片,可赞AI都能让信息处理更清晰高效。覆盖数据汇报、会议纪要、内容营销等全场景,大幅提升办公效率,降低专业门槛,是您提升工作效率的得力助手。
- 3418次使用
-
- 星月写作
- 星月写作是国内首款聚焦中文网络小说创作的AI辅助工具,解决网文作者从构思到变现的全流程痛点。AI扫榜、专属模板、全链路适配,助力新人快速上手,资深作者效率倍增。
- 4523次使用
-
- MagicLight
- MagicLight.ai是全球首款叙事驱动型AI动画视频创作平台,专注于解决从故事想法到完整动画的全流程痛点。它通过自研AI模型,保障角色、风格、场景高度一致性,让零动画经验者也能高效产出专业级叙事内容。广泛适用于独立创作者、动画工作室、教育机构及企业营销,助您轻松实现创意落地与商业化。
- 3797次使用
-
- 提升Java功能开发效率的有力工具:微服务架构
- 2023-10-06 501浏览
-
- 掌握Java海康SDK二次开发的必备技巧
- 2023-10-01 501浏览
-
- 如何使用java实现桶排序算法
- 2023-10-03 501浏览
-
- Java开发实战经验:如何优化开发逻辑
- 2023-10-31 501浏览
-
- 如何使用Java中的Math.max()方法比较两个数的大小?
- 2023-11-18 501浏览

