1. 简介

本篇快速教程将带你通过一个使用 JPA 注解实现一对多映射的示例,作为 XML 配置的替代方案。

我们还将学习什么是双向关系,它们如何导致数据不一致,以及“关系拥有方”的概念如何解决这个问题。

2. 描述

简单来说,一对多映射意味着一个表中的一行数据对应另一个表中的多行数据

看下面的实体关系图来理解一对多关联:

C-1

本示例中,我们将实现一个购物车系统:一个表存储购物车信息,另一个表存储商品信息。一个购物车可以包含多个商品,这就是典型的一对多映射

在数据库层面,我们通过在 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>

最新版本请查阅 HibernateH2 的 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,但操作失误导致 cart2item2 的引用不一致:

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,但数据库中保存的是 item2cart2 的引用。

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
  • 设置多对一方的 @JoinColumninsertable=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 获取。


原始标题:Hibernate One to Many Annotation Tutorial