
本文深入探讨了在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 的反向关联。
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; }
}在 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); // 保存更新
}
}以上就是JPA多对多关系与中间表映射实践指南的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号