1. 简介
本篇快速教程将带你通过一个使用 JPA 注解实现一对多映射的示例,作为 XML 配置的替代方案。
我们还将学习什么是双向关系,它们如何导致数据不一致,以及“关系拥有方”的概念如何解决这个问题。
2. 描述
简单来说,一对多映射意味着一个表中的一行数据对应另一个表中的多行数据。
看下面的实体关系图来理解一对多关联:
本示例中,我们将实现一个购物车系统:一个表存储购物车信息,另一个表存储商品信息。一个购物车可以包含多个商品,这就是典型的一对多映射。
在数据库层面,我们通过在 cart
表中使用 cart_id
作为主键,同时在 items
表中使用 cart_id
作为外键来实现这种关系。
在代码中,我们使用 @OneToMany
注解来表示这种映射:
public class Cart {
//...
@OneToMany(mappedBy="cart")
private Set<Item> items;
//...
}
我们还可以在 Item
类中通过 @ManyToOne
添加对 Cart
的引用,从而建立双向关系。双向意味着我们可以从购物车访问商品集合,也能从商品访问所属购物车。
mappedBy
属性用于告诉 Hibernate,在子类中哪个变量代表父类。
开发示例应用需要以下技术和库:
- JDK 1.8+
- Hibernate 6
- Maven 3+
- H2 数据库
3. 环境搭建
3.1 数据库设置
我们将使用 Hibernate 根据领域模型自动管理数据库模式。也就是说,不需要手动编写创建表和关系的 SQL 语句。直接进入项目创建环节。
3.2 Maven 依赖
在 pom.xml
中添加 Hibernate 和 H2 驱动依赖。Hibernate 依赖会自动引入 JBoss 日志等传递依赖:
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
<version>6.4.2.Final</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.2.224</version>
</dependency>
最新版本请查阅 Hibernate 和 H2 的 Maven 中央仓库。
3.3 Hibernate SessionFactory
创建用于数据库交互的 Hibernate SessionFactory
:
public static SessionFactory getSessionFactory() {
ServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder()
.applySettings(dbSettings())
.build();
Metadata metadata = new MetadataSources(serviceRegistry)
.addAnnotatedClass(Cart.class)
// 其他领域类
.buildMetadata();
return metadata.buildSessionFactory();
}
private static Map<String, Object> dbSettings() {
// 返回 Hibernate 配置
}
4. 模型定义
在模型类中使用 JPA 注解完成映射配置:
@Entity
@Table(name="CART")
public class Cart {
//...
@OneToMany(mappedBy="cart")
private Set<Item> items;
// getters and setters
}
注意:@OneToMany
注解指定了 Item
类中用于映射的变量(即 mappedBy
指向的属性)。这就是为什么 Item
类中需要有一个名为 cart
的属性:
@Entity
@Table(name="ITEMS")
public class Item {
//...
@ManyToOne
@JoinColumn(name="cart_id", nullable=false)
private Cart cart;
public Item() {}
// getters and setters
}
关键点:
@ManyToOne
注解关联到Cart
类变量@JoinColumn
注解指定外键列名
5. 实战演示
在测试程序中,我们创建一个包含 main()
方法的类,获取 Hibernate Session 并保存模型对象:
sessionFactory = HibernateAnnotationUtil.getSessionFactory();
session = sessionFactory.getCurrentSession();
LOGGER.info("Session created");
tx = session.beginTransaction();
session.save(cart);
session.save(item1);
session.save(item2);
tx.commit();
LOGGER.info("Cart ID={}", cart.getId());
LOGGER.info("item1 ID={}, Foreign Key Cart ID={}", item1.getId(), item1.getCart().getId());
LOGGER.info("item2 ID={}, Foreign Key Cart ID={}", item2.getId(), item2.getCart().getId());
程序输出结果:
Session created
Hibernate: insert into CART values ()
Hibernate: insert into ITEMS (cart_id)
values (?)
Hibernate: insert into ITEMS (cart_id)
values (?)
Cart ID=7
item1 ID=11, Foreign Key Cart ID=7
item2 ID=12, Foreign Key Cart ID=7
Closing SessionFactory
6. @ManyToOne 注解详解
如第 2 节所见,我们可以用 @ManyToOne
注解定义多对一关系。多对一映射表示多个实体实例对应另一个实体的一个实例——多个商品属于一个购物车。
@ManyToOne
注解也能创建双向关系。下面几个小节将详细说明。
6.1 数据不一致与关系拥有方
如果只有 Cart
引用 Item
,而 Item
不反向引用 Cart
,关系就是单向的,对象间自然保持一致性。
但在我们的双向关系中,可能出现数据不一致。
假设开发者想把 item1
加入 cart1
,把 item2
加入 cart2
,但操作失误导致 cart2
和 item2
的引用不一致:
Cart cart1 = new Cart();
Cart cart2 = new Cart();
Item item1 = new Item(cart1);
Item item2 = new Item(cart2);
Set<Item> itemsSet = new HashSet<Item>();
itemsSet.add(item1);
itemsSet.add(item2);
cart1.setItems(itemsSet); // 错误操作!
如上所示:item2
引用 cart2
,但 cart2
的商品集合中却没有 item2
——这就踩坑了!
Hibernate 该如何保存 item2
? 它的外键应该指向 cart1
还是 cart2
?
我们通过关系拥有方的概念解决这种歧义:拥有方的引用具有更高优先级,会被保存到数据库。
6.2 将 Item 设为拥有方
根据 JPA 规范 2.9 节,最佳实践是将多对一方设为拥有方。
换句话说:Item
是拥有方,Cart
是反向方——这正是我们之前的实现方式。
如何实现的?
- ✅ 在
Cart
类中使用mappedBy
属性,标记其为反向方 - ✅ 在
Item.cart
字段上使用@ManyToOne
,使Item
成为拥有方
回到之前的“不一致”示例:现在 Hibernate 知道 item2
的引用更重要,会将其保存到数据库。
验证结果:
item1 ID=1, Foreign Key Cart ID=1
item2 ID=2, Foreign Key Cart ID=2
虽然代码中 cart1
引用了 item2
,但数据库中保存的是 item2
对 cart2
的引用。
6.3 将 Cart 设为拥有方
也可以将一对多方设为拥有方,多对一方设为反向方(⚠️ 不推荐,但可以尝试)。
实现代码如下:
public class ItemOIO {
// ...
@ManyToOne
@JoinColumn(name = "cart_id", insertable = false, updatable = false)
private CartOIO cart;
//..
}
public class CartOIO {
//..
@OneToMany
@JoinColumn(name = "cart_id") // 需要重复定义物理信息
private Set<ItemOIO> items;
//..
}
关键修改:
- 移除
mappedBy
- 设置多对一方的
@JoinColumn
为insertable=false, updatable=false
运行相同代码,结果完全相反:
item1 ID=1, Foreign Key Cart ID=1
item2 ID=2, Foreign Key Cart ID=1
现在 item2
属于 cart1
了。
7. 总结
我们看到了使用 Hibernate ORM 和 H2 数据库通过 JPA 注解实现一对多关系是多么简单。
此外,我们还学习了双向关系的概念,以及如何通过关系拥有方解决数据不一致问题。
本文源代码可在 GitHub 获取。