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

  n-n(多对多)的关联关系必须通过连接表实现。下面以商品种类和商品之间的关系,即一个商品种类下面可以有多种商品,一种商品又可以属于多个商品种类,分别介绍单向的n-n关联关系和双向的n-n关联关系。

单向的n-n关联关系

  如果仅使用两张数据表,是不能实现n-n的关联关系的,如下图:
  image_1b39rjpnd1pi81qv738a15pi1rbv13.png-27.6kB
  商品ITEM_AA属于商品种类CATEGORY_AA,但是如果商品ITEM_AA又同时属于商品种类CATEGORY_BB呢,两张数据表无法实现这种关系,所以我们需要使用到连接表,如下图所示:
  image_1b39rmve8rui1n6u4lemavnk1g.png-34.6kB
  我们添加一张连接表,其中的每一条记录表示某一个商品和某一个商品种类的对应关系。
  单向的n-n关联关系的域模型和关系数据模型如下图所示:
  image_1b39rdrgb16fp143n1hrtkv23fum.png-82.5kB
  下面来实现这种关系,首先新建两个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Category {
private Integer id;
private String name;
private Set<Item> items = new HashSet<>();
//getters and setters
}
public class Item {
private Integer id;
private String name;
//getters and setters
}

生成并编辑映射文件:
 
Category.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
<?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.n2n">
<class name="Category" table="CATEGORIES">
<id name="id" type="java.lang.Integer">
<column name="ID" />
<generator class="native" />
</id>
<property name="name" type="java.lang.String">
<column name="NAME" />
</property>
<!-- table: 指定中间表 -->
<set name="items" table="CATEGORIES_ITEMS">
<key>
<column name="C_ID" />
</key>
<!-- 使用 many-to-many 指定多对多的关联关系. column 执行 Set 集合中的持久化类在中间表的外键列的名称 -->
<many-to-many class="Item" column="I_ID"></many-to-many>
</set>
</class>
</hibernate-mapping>

Item.hbm.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?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.n2n.Item" table="ITEMS">
<id name="id" type="java.lang.Integer">
<column name="ID" />
<generator class="native" />
</id>
<property name="name" type="java.lang.String">
<column name="NAME" />
</property>
</class>
</hibernate-mapping>

  与1-n关联关系类似,在映射时,必须在category类的映射文件中设置set节点。但是不同的是,在set节点中指定的表为连接表CATEGORIES_ITEMS,而不再是所关联的类Item所对应的表,而且,在set节点中设置的是many-to-many节点,而不再是one-to-many。many-to-many节点的class属性指定了items集合中存放的对象类型是Item,colume属性指定了连接表CATEGORIES_ITEMS中参照ITEMS表的外键是ITEM_ID。
  现在随便一个测试程序,发现除了生成了表CATEGORIES和表ITEMS,还生成了连接表CATEGORIES_ITEMS,表结构为:
  image_1b39sup4kk251768905q88mp3h.png-17.9kB
   
  image_1b39srd5u68vmnn1rboe841m8c2a.png-19.8kB
  
  image_1b39ss3o61adjk5i11pbcorv692n.png-16.6kB
  image_1b39st1rd1ve880v139411iec2o34.png-20.4kB
  
下面测试save操作和get操作:
  
单向n-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
@Test
public void testSave(){
Category category1 = new Category();
category1.setName("C-AA");
Category category2 = new Category();
category2.setName("C-BB");
Item item1 = new Item();
item1.setName("I-AA");
Item item2 = new Item();
item2.setName("I-BB");
//设定关联关系
category1.getItems().add(item1);
category1.getItems().add(item2);
category2.getItems().add(item1);
category2.getItems().add(item2);
//执行保存操作
session.save(category1);
session.save(category2);
session.save(item1);
session.save(item2);
}

  运行程序,根据控制台打印的sql语句,发现会先往CATEGORIES表和ITEMS表分别插入两条记录,然后再往连接表CATEGORIES_ITEMS插入四条记录,这是符合我们的预期的:
  image_1b39tavqsr1pt8k1p2t1t4obkh3u.png-45.7kB
  image_1b39tbqdjs8b5gv6jc1q3tob74b.png-35.3kB
   
  image_1b39tc9ha1vs4ghpk69184lfi4o.png-12kB
  image_1b39tcgol9ki11v81o2bjgb1i3e55.png-12.5kB
  image_1b39tcqfl1q8k72e1fd4n2117gp5i.png-10.9kB
  
单向n-n关联关系的get操作:

1
2
3
4
5
6
7
8
9
@Test
public void testGet(){
Category category = (Category) session.get(Category.class, 1);
System.out.println(category.getName());
//需要连接中间表
Set<Item> items = category.getItems();
System.out.println(items.size());
}

  同之前的1-n、1-1中类似,先查询category会使用懒加载机制,不会立即加载items,只有使用到items的时候才会加载。当加载items的时候,是通过内连接连接中间表CATEGORIES_ITEMS进行查询的:
  image_1b39u7n5a1qfflau1q9d1n9v1kd55v.png-32.1kB   

双向的n-n关联关系

  在双向的n-n关联关系中,两端的类都需要使用集合属性,即Category中要包含一个Set<Item>,Item中也要包含一个Set<Category>,,且在数据库中同样需要使用连接表连接。其域模型和关系数据模型如下:
  image_1b39ucin71p3okqd5vhhjop4j6c.png-84kB
  
  下面编写代码,首先创建两个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Category {
private Integer id;
private String name;
private Set<Item> items = new HashSet<>();
//getters and setters
}
public class Item {
private Integer id;
private String name;
private Set<Category> categories = new HashSet<>();
//getters and setters
}

生成并编辑映射文件:
 
Category.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
<?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.n2n">
<class name="Category" table="CATEGORIES">
<id name="id" type="java.lang.Integer">
<column name="ID" />
<generator class="native" />
</id>
<property name="name" type="java.lang.String">
<column name="NAME" />
</property>
<!-- table: 指定中间表 -->
<set name="items" table="CATEGORIES_ITEMS">
<key>
<column name="C_ID" />
</key>
<!-- 使用 many-to-many 指定多对多的关联关系. column 执行 Set 集合中的持久化类在中间表的外键列的名称 -->
<many-to-many class="Item" column="I_ID"></many-to-many>
</set>
</class>
</hibernate-mapping>

Item.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
<?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.n2n.Item" table="ITEMS">
<id name="id" type="java.lang.Integer">
<column name="ID" />
<generator class="native" />
</id>
<property name="name" type="java.lang.String">
<column name="NAME" />
</property>
<set name="categories" table="CATEGORIES_ITEMS" inverse="true">
<key column="I_ID"></key>
<many-to-many class="com.atguigu.hibernate.n2n.Category" column="C_ID"></many-to-many>
</set>
</class>
</hibernate-mapping>

  大部门内容和单向n-n中类似,只是需要在Item类中添加字段

1
  private Set<Category> categories = new HashSet<>();

并且需要在Item类的映射文件中像Category映射文件中那样配置set节点。此外,还有一个很重要的地方,就是需要在Category和Item的其中一端的映射文件中的set节点中设置inverse=”true”属性,以放弃维护关联关系,否则两端都会维护默认关系,这在某些情况下会导致错误,下面会演示。
  现在生成数据表,表结构和单向n-n中一样:
  image_1b39sup4kk251768905q88mp3h.png-17.9kB
   
  image_1b39srd5u68vmnn1rboe841m8c2a.png-19.8kB
  
  image_1b39ss3o61adjk5i11pbcorv692n.png-16.6kB
  image_1b39st1rd1ve880v139411iec2o34.png-20.4kB

  下面测试save操作和get操作:
  
双向n-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
Category category1 = new Category();
category1.setName("C-AA");
Category category2 = new Category();
category2.setName("C-BB");
Item item1 = new Item();
item1.setName("I-AA");
Item item2 = new Item();
item2.setName("I-BB");
//设定关联关系
category1.getItems().add(item1);
category1.getItems().add(item2);
category2.getItems().add(item1);
category2.getItems().add(item2);
item1.getCategories().add(category1);
item1.getCategories().add(category2);
item2.getCategories().add(category1);
item2.getCategories().add(category2);
//执行保存操作
session.save(category1);
session.save(category2);
session.save(item1);
session.save(item2);

  运行程序,和单向n-n中一样,一共打印8条insert语句,并成功插入记录。但是,现在我们移除Item.hbm.xml文件中的inverse=”true”属性,再运行程序,会抛出异常。这是因为在默认情况下n-n的两端都会维护关联关系,当执行上述四条save代码后,category要维护关联关系,往连接表中添加4条记录,然后item也要维护关联关系,往连接表中添加相同的4条记录,会导致连接表中主键重复,解决方法就是在Item.hbm.xml文件中设置inverse=”true”属性。
双向n-n关联关系的get操作:

1
2
3
4
5
6
7
8
9
@Test
public void testGet(){
Category category = (Category) session.get(Category.class, 1);
System.out.println(category.getName());
//需要连接中间表
Set<Item> items = category.getItems();
System.out.println(items.size());
}

  双向n-n关联关系的get操作和单向n-n中一样,包含懒加载机制和内连接。
  补充一下,双向n-n中两端的映射文件的字段对应如下图所示:
  image_1b3a0lgva19hamhlrjs1g461gj66p.png-146.2kB