Hibernate的一对多关联关系(单向和双向)

  在领域模型中,类与类之间最普遍的关系就是关联关系。在 UML 中,关联是有方向的。以 Customer 和 Order 为例:一个用户能发出多个订单, 而一个订单只能属于一个客户。从 Order 到 Customer 的关联是多对一关联; 而从 Customer 到 Order 是一对多关联。   

单向n-1的关联关系

  单向 n-1 关联只需从 n 的一端可以访问 1 的一端。
  域模型:从 Order 到 Customer 的多对一单向关联需要在Order 类中定义一个 Customer 属性,而在 Customer 类中无需定义存放 Order 对象的集合属性。
  关系数据模型: ORDERS 表中的 CUSTOMER_ID 参照 CUSTOMER 表的主键。(外键)
  Hibernate 使用 元素来映射多对一关联关系。
  例如,在Customer和Order的例子中,首先创建两个类文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Customer {
private Integer customerId;
private String customerName;
//getters and setters
}
public class Order {
private Integer orderId;
private String orderName;
private Customer customer;
//getters and setters
}

生成hibernate映射文件:

Customer.hbm.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="com.atguigu.hibernate.entities.n21.Customer" table="CUSTOMERS">
<id name="customerId" type="java.lang.Integer">
<column name="CUSTOMER_ID" />
<generator class="native" />
</id>
<property name="customerName" type="java.lang.String">
<column name="CUSTOMER_NAME" />
</property>
</class>
</hibernate-mapping>

Order.hbm.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="com.atguigu.hibernate.entities.n21">
<class name="Order" table="ORDERS">
<id name="orderId" type="java.lang.Integer">
<column name="ORDER_ID" />
<generator class="native" />
</id>
<property name="orderName" type="java.lang.String">
<column name="ORDER_NAME" />
</property>
<!--
映射多对一的关联关系。 使用 many-to-one 来映射多对一的关联关系
name: 多这一端关联的一那一端的属性的名字
class: 一那一端的属性对应的类名
column: 一那一端在多的一端对应的数据表中的外键的名字
-->
<many-to-one name="customer" class="Customer" column="CUSTOMER_ID"></many-to-one>
</class>
</hibernate-mapping>

先随便运行一个程序,来生成数据库表:
 
customers表
image_1b34sm9cq1m8p194v1e3p1jdlmnk9.png-21.3kB

orders表
image_1b34sn5651s8a1es8j9e1sar1sm2m.png-25.8kB

image_1b34snfoddfq15l2mn2i7r5mr13.png-17.7kB
 
单向n-1的save操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Test
public void testMany2OneSave(){
Customer customer = new Customer();
customer.setCustomerName("AA");
Order order1 = new Order();
order1.setOrderName("ORDER-1");
Order order2 = new Order();
order2.setOrderName("ORDER-2");
//设定关联关系
order1.setCustomer(customer);
order2.setCustomer(customer);
//执行 save 操作: 先插入 Customer, 再插入 Order, 3 条 INSERT
//先插入 1 的一端, 再插入 n 的一端, 只有 INSERT 语句.
session.save(customer);
session.save(order1);
session.save(order2);
//先插入 Order, 再插入 Customer. 3 条 INSERT, 2 条 UPDATE
//先插入 n 的一端, 再插入 1 的一端, 会多出 UPDATE 语句!
//因为在插入多的一端时, 无法确定 1 的一端的外键值. 所以只能等 1 的一端插入后, 再额外发送 UPDATE 语句.
//推荐先插入 1 的一端, 后插入 n 的一端
//session.save(order1);
//session.save(order2);
//session.save(customer);
}
}

运行程序,可以成功插入记录,并且在控制台只会打印三条insert语句:

image_1b34t38nn1q6e1lavbqclas1oka1g.png-11.8kB

image_1b34t3h021na01pbno00vp01g4m1t.png-11.7kB

image_1b34t4fap1gq41b5in52i8v1vmd2a.png-37.4kB

但是,如果注释掉倒数4,5,6行的代码,而使用最后的三行代码,即先保存order1和order2,再保存customer,同样也可以成功插入,但是除了会输出三行insert语句,还会输出两行update语句。如下图:
image_1b34tf2baivg10kv1bplc931npu2n.png-28.7kB
 
这是因为在先插入order记录时,无法确定外键值customer_id,只能先置为null,所以只能等customer记录插入后,再额外发送 UPDATE 语句去更新customer_id。所以,建议先插入1的那一端,即customer,后插入n的这一端,即order。

单向n-1的get操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void testMany2OneGet(){
//1. 若查询多的一端的一个对象, 则默认情况下, 只查询了多的一端的对象. 而没有查询关联的 1 的那一端的对象!
Order order = (Order) session.get(Order.class, 1);
System.out.println(order.getOrderName());
System.out.println(order.getCustomer().getClass().getName());
//session.close();
//2. 只有在需要使用到关联的对象时, 才发送对应的 SQL 语句.
Customer customer = order.getCustomer();
System.out.println(customer.getCustomerName());
//3. 在查询 Customer 对象时, 由多的一端导航到 1 的一端时,
//若此时 session 已被关闭, 则默认情况下
//会发生 LazyInitializationException 异常
//4. 获取 Order 对象时, 默认情况下, 其关联的 Customer 对象是一个代理对象!
}

在n-1的get操作中,使用了懒加载机制。如果查询的是order对象,则默认情况下不会立即查找对应的customer对象,而只有等到需要使用这个customer对象时,才会发送select语句查询该customer对象。那么当然,如果在使用该对象之前,session被关闭了,也会抛出懒加载异常。

单向n-1的update操作:

1
2
3
4
5
@Test
public void testUpdate(){
Order order = (Order) session.get(Order.class, 1);
order.getCustomer().setCustomerName("AAA");
}

单向n-1的delete操作:

1
2
3
4
5
6
@Test
public void testDelete(){
//在不设定级联关系的情况下, 且 1 这一端的对象有 n 的对象在引用, 不能直接删除 1 这一端的对象
Customer customer = (Customer) session.get(Customer.class, 1);
session.delete(customer);
}

在不设定级联关系的情况下,且 1 这一端的对象有 n 的对象在引用,则不能直接删除 1 这一端的对象。我们可以在customer映射文件的set节点中设置级联属性为级联删除,就可以直接删除1这一端的对象:

1
<set name="orders" table="ORDERS" cascade="delete">

除了级联删除之外,还有其他的级联属性,如下图所示:
image_1b352uke6ftd1kj45n93jejci4o.png-217.9kB

但是在开发时并不建议设定级联属性,而建议使用手工的方式来处理。  

双向1-n的关联关系

  双向 1-n 与双向 n-1 是完全相同的两种情形。
  双向 1-n 需要在1的一端可以访问n的一端,反之亦然。
  域模型:从Order到Customer的多对一双向关联需要在Order类中定义一个Customer属性,而在Customer类中需定义存放Order对象的集合属性。
  关系数据模型: ORDERS表中的CUSTOMER_ID参照CUSTOMER表的主键。(外键)
  
几个注意点:

  1. 当 Session从数据库中加载Java集合时,创建的是Hibernate内置集合类的实例,因此在持久化类中定义集合属性时必须把属性声明为Java 接口类型,例如应该声明为Set而不是HashSet。Hibernate 的内置集合类具有集合代理功能,支持延迟检索策略。类似于在单向n-1关系的get操作,如果在双向1-n的get操作中获取了customer对象,如果不使用它存放order的集合,那么这个集合就不会被加载,只有使用到时才会加载。
  2. 在customer类中定义集合属性时,通常把它初始化为集合实现类的一个实例,这样可以提高程序的健壮性,避免应用程序访问取值为null的集合的方法抛出NullPointerException。

    Hibernate 使用 元素来映射set类型的属性。下面我们仍以customer和order的例子来测试,首先创建两个类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    public class Customer {
    private Integer customerId;
    private String customerName;
    /*
    * 1. 声明集合类型时, 需使用接口类型, 因为 hibernate 在获取
    * 集合类型时, 返回的是 Hibernate 内置的集合类型, 而不是 JavaSE 一个标准的集合实现.
    * 2. 需要把集合进行初始化, 可以防止发生空指针异常
    */
    private Set<Order> orders = new HashSet<>();
    //getters and setters
    }
    public class Order {
    private Integer orderId;
    private String orderName;
    private Customer customer;
    //getters and setters
    }

生成Hibernate映射文件:

Order.hbm.xml(和单向n-1相同)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="com.atguigu.hibernate.entities.n21.both">
<class name="Order" table="ORDERS">
<id name="orderId" type="java.lang.Integer">
<column name="ORDER_ID" />
<generator class="native" />
</id>
<property name="orderName" type="java.lang.String">
<column name="ORDER_NAME" />
</property>
<!--
映射多对一的关联关系。 使用 many-to-one 来映射多对一的关联关系
name: 多这一端关联的一那一端的属性的名字
class: 一那一端的属性对应的类名
column: 一那一端在多的一端对应的数据表中的外键的名字
-->
<many-to-one name="customer" class="Customer" column="CUSTOMER_ID"></many-to-one>
</class>
</hibernate-mapping>

Customer.hbm.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="com.atguigu.hibernate.entities.n21.both">
<class name="Customer" table="CUSTOMERS">
<id name="customerId" type="java.lang.Integer">
<column name="CUSTOMER_ID" />
<generator class="native" />
</id>
<property name="customerName" type="java.lang.String">
<column name="CUSTOMER_NAME" />
</property>
<!-- 映射 1 对多的那个集合属性 -->
<!-- set: 映射 set 类型的属性, name:一的这一端关联的多的那一端的属性名,table: set 中的元素对应的记录放在哪一个数据表中. 该值需要和多对一的多的那个表的名字一致 -->
<set name="orders" table="ORDERS">
<!-- 指定关联的表中的外键列的名字 -->
<key column="CUSTOMER_ID"></key>
<!-- 指定映射类型 -->
<one-to-many class="Order"/>
</set>
</class>
</hibernate-mapping>

生成的数据库表的结构和单向n-1中相同,现在来测试各种方法:

双向1-n的save操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Test
public void testOne2ManySave(){
Customer customer = new Customer();
customer.setCustomerName("AA");
Order order1 = new Order();
order1.setOrderName("ORDER-1");
Order order2 = new Order();
order2.setOrderName("ORDER-2");
//设定关联关系
order1.setCustomer(customer);
order2.setCustomer(customer);
customer.getOrders().add(order1);
customer.getOrders().add(order2);
//执行 save 操作: 先插入 Customer, 再插入 Order, 3 条 INSERT, 2 条 UPDATE
//因为 1 的一端和 n 的一端都维护关联关系. 所以会多出 UPDATE
//可以在 1 的一端的 set 节点指定 inverse=true, 来使 1 的一端放弃维护关联关系!
//建议设定 set 的 inverse=true, 建议先插入 1 的一端, 后插入多的一端
//好处是不会多出 UPDATE 语句
session.save(customer);
session.save(order1);
session.save(order2);
//先插入 Order, 再插入 Cusomer, 3 条 INSERT, 4 条 UPDATE
//session.save(order1);
//session.save(order2);
//session.save(customer);
}
}

当我们使用倒数第4,5,6行代码进行插入,即先保存customer,再保存order1和order2,这不同于单向n-1中的操作,虽然插入都是成功的,但是此处除了会打印三条insert语句,还会打印两条update语句:
image_1b3509k521lab126n1k8fmk5v5934.png-26.1kB

这是因为, 由于是双向的关联关系,所以 1 的一端和 n 的一端都需要维护关联关系。当先插入customer对象后,customer中的set集合里面的order的id也还未知,会先被置为null,所以当order1和order2被插入后,会多出两条update语句。那么可以推测出,如果注释掉倒数第4,5,6行代码,而执行最后三行代码,那么除了有三条insert语句,还会打印出4条update语句,因为order1和order2先各维护一次,customer会再维护两次。如下图:

image_1b350gg27r8e1foo45umhn1khs3h.png-30kB

  那么,如果我们不希望两端都维护关联关系,该怎么办呢?
  解决办法是,在hibernate的配置文件中可以通过设置inverse属性来决定是由双向关联的哪一方来维护表和表之间的关系。inverse = false的为主动方,inverse = true 的为被动方。由主动方负责维护关 联关系。在没有设置inverse属性的情况下,默认父子两边都维护父子关系。
  在双向 1-n 关系中,将 n 方设为主控方将有助于性能改善,而如果将 1 方设为主控方会额外多出update语句。这好比如果要国家元首记住全国人民的名字不太现实,但要让全国人民知道国家元首,就容易得多。
  例如,现在我们在Customer.hbm.xml映射文件中设置set节点为:

1
<set name="orders" table="ORDERS" inverse="true">

然后在test方法中先插入customer,再插入order1和order2,则只会打印三条insert语句,而不会打印update语句:

image_1b3510fhr1i82j7n1krfi6ufcf3u.png-32.6kB

双向1-n的get操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void testOne2ManyGet(){
//1. 对 n 的一端的集合使用延迟加载
Customer customer = (Customer) session.get(Customer.class, 7);
System.out.println(customer.getCustomerName());
//2. 返回的多的一端的集合时 Hibernate 内置的集合类型.
//该类型具有延迟加载和存放代理对象的功能.
System.out.println(customer.getOrders().getClass());
//session.close();
//3. 可能会抛出 LazyInitializationException 异常
System.out.println(customer.getOrders().size());
//4. 再需要使用集合中元素的时候进行初始化.
}

  与单向n-1的get操作类似,在双向1-n的get操作中,如果先加载了customer对象,在使用它的orders集合之前,是不会加载orders集合的,这使用了懒加载机制,那么同样,也有可能抛出懒加载异常。
  
  
双向1-n的update操作:

1
2
3
4
5
@Test
public void testUpdat2(){
Customer customer = (Customer) session.get(Customer.class, 1);
customer.getOrders().iterator().next().setOrderName("GGG");
}

可以通过1这一端的customer来更新n那一端的order。
 
双向1-n的delete操作:

1
2
3
4
5
6
@Test
public void testDelete(){
//在不设定级联关系的情况下, 且 1 这一端的对象有 n 的对象在引用, 不能直接删除 1 这一端的对象
Customer customer = (Customer) session.get(Customer.class, 1);
session.delete(customer);
}

同单向n-1中一样,在不设定级联关系的情况下,且 1 这一端的对象有 n 的对象在引用,则不能直接删除 1 这一端的对象。
 
 
  此外,我们还可以在customer映射文件的set节点中设置order-by属性, 在查询时对集合中的元素进行排序,order-by 中使用的是表的字段名, 而不是持久化类的属性名。例如,对orders集合根据orderName进行降序排列:

1
<set name="orders" table="ORDERS" order-by="ORDER_NAME DESC">

  最后补充一下,关于双向1-n的两个类的映射文件中,属性的对应关系,如下图所示:
  image_1b3523tsj1ueu10p2bu7n031bms4b.png-177.1kB