Hibernate的一对一关联关系

  Hibernate的一对一关联关系,分为基于外键的一对一关联关系和基于主键的一对一关联关系。在这篇文章中,我们以部门和部门经理的例子来说明,一个部门对应唯一一个部门经理,一个部门经理也对应唯一一个部门。
  在基于外键的一对一关联关系中,一端通过一个主键以外的字段关联另一端的主键,如下图所示:
  E3CF6A0B-55EB-43AE-BDBC-D2DB80C116EF.jpg-8.3kB
   
  在基于主键的一对一关联关系中,一端直接通过主键关联另一端的主键,并通过另一端的主键生成自己的主键,如下图所示:
  image_1b36lr2j11khlidodn5i5e1net12.png-22.9kB
   
  下面进行详细说明。

基于外键的一对一关联关系

  对于基于外键的1-1关联关系,外键可以存放在任意一端,例如在本例中,外键既可以存放在department一端,也可以存放在manager一端,我们假设存放在department一端。在需要存放外键的一端,增加many-to-one节点,并且为many-to-one节点添加unique=”true”属性,来表示为1-1关联,添加unique=”true”属性了以后,不同的department就不能关联同一个manager了。在不存放外键的一端,需要使用one-to-one节点,并且在该节点中添加property-ref属性来指定存放外键一端的除主键以外的字段来作为关联字段。代码如下,首先创建两个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Department {
private Integer deptId;
private String deptName;
private Manager mgr;
//getters and setters
}
public class Manager {
private Integer mgrId;
private String mgrName;
private Department dept;
//getters and setters
}

映射文件:
 
Department.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
<?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.one2one.foreign.Department" table="DEPARTMENTS">
<id name="deptId" type="java.lang.Integer">
<column name="DEPT_ID" />
<generator class="native" />
</id>
<property name="deptName" type="java.lang.String">
<column name="DEPT_NAME" />
</property>
<!-- 使用 many-to-one 的方式来映射 1-1 关联关系 -->
<!-- 添加unique="true"属性,来表示为1-1关联 -->
<many-to-one name="mgr" class="com.atguigu.hibernate.one2one.foreign.Manager"
column="MGR_ID" unique="true"></many-to-one>
</class>
</hibernate-mapping>

Manager.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>
<class name="com.atguigu.hibernate.one2one.foreign.Manager" table="MANAGERS">
<id name="mgrId" type="java.lang.Integer">
<column name="MGR_ID" />
<generator class="native" />
</id>
<property name="mgrName" type="java.lang.String">
<column name="MGR_NAME" />
</property>
<!-- 映射 1-1 的关联关系: 在对应的数据表中已经有外键了, 当前持久化类使用 one-to-one 进行映射 -->
<!--
没有外键的一端需要使用one-to-one元素,该元素使用 property-ref 属性指定使用被关联实体主键以外的字段作为关联字段
-->
<one-to-one name="dept"
class="com.atguigu.hibernate.one2one.foreign.Department"
property-ref="mgr"></one-to-one>
</class>
</hibernate-mapping>

运行一个空的test程序,可以生成数据库表managers和departments:
 
image_1b36mlbre1tev98d1f9rusbiu91f.png-19kB
 
image_1b36mmdvq19lu1o681ehs1sgc1gmh1s.png-19.8kB
image_1b36mmu0n1qp61tpek735sbdfr29.png-18.9kB

下面测试基于外键的1-1关联关系的save和get操作:

基于外键的1-1关联关系的save操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void testSave(){
Department department = new Department();
department.setDeptName("DEPT-BB");
Manager manager = new Manager();
manager.setMgrName("MGR-BB");
//设定关联关系
department.setMgr(manager);
manager.setDept(department);
//保存操作
//建议先保存没有外键列的那个对象. 这样会减少 UPDATE 语句
session.save(manager);
session.save(department);
}

  这段代码可以正常插入记录。和n-1关联关系中一样,如果先保存了存放外键一端的对象,后保存被外键关联的一端的对象,即如果先执行session.save(department);,后执行session.save(manager);,虽然同样可以正确保存,但是会多出一条update语句用于维护关联关系,所以通常建议先插入没有外键一端的对象,后插入有外键一端的对象。
  
基于外键的1-1关联关系的get操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void testGet(){
//1. 默认情况下对关联属性使用懒加载
Department dept = (Department) session.get(Department.class, 1);
System.out.println(dept.getDeptName());
//2. 所以会出现懒加载异常的问题.
// session.close();
// Manager mgr = dept.getMgr();
// System.out.println(mgr.getClass());
// System.out.println(mgr.getMgrName());
//3. 查询 Manager 对象的连接条件应该是 dept.manager_id = mgr.manager_id
//而不应该是 dept.dept_id = mgr.manager_id
Manager mgr = dept.getMgr();
System.out.println(mgr.getMgrName());
}

  同n-1关联关系一样,当查询存放外键的一端的对象department的时候,使用懒加载机制,即不会立即加载它关联的另一端的对象manager,而只有等到要使用manager的时候,才会发送select语句加载。那么同样也有可能发生懒加载异常。运行结果如下图所示:
  image_1b36o9f1j1a1mss9i951atf1cqh2m.png-65.3kB
  值得注意的是,当要使用到manager对象时,是通过左外连接查询到manager对象的,连接条件是dept.manager_id = mgr.manager_id,这是正确的,因为我们在Manager.hbm.xml中的one-to-one节点中配置了property-ref=”mgr”,指定关联字段为department的mgr字段。如果没有设置property-ref属性,那么默认关联的字段为department的id字段,例如,我们去掉property-ref=”mgr”的设置,运行testGet()方法,则会打印如下的sql语句(连接条件是dept.dept_id = mgr.manager_id),这显然是不符合需求的。
  image_1b36vjp9k2tp1jum6srtu7bs833.png-35.3kB
  
  还有一个注意点,就是当首先查询不存放外键的一端的对象时,即manager,由于其中没有设置外键关联到department,所以会使用左外连接查询,一并查询出另一端的对象,即department,而且已经完成了初始化。如下所示:

1
2
3
4
5
6
7
8
9
10
@Test
public void testGet2(){
//在查询没有外键的实体对象时, 使用的左外连接查询, 一并查询出其关联的对象
//并已经进行初始化.
Manager mgr = (Manager) session.get(Manager.class, 1);
//在执行下面的代码之前已经完成了对department对象的初始化
System.out.println(mgr.getMgrName());
System.out.println(mgr.getDept().getDeptName());
}

运行结果:
image_1b37027r68l01dvj1j2u1dnu14ua3t.png-33.9kB
  
  关于基于外键的一对一关系,还有一点值得注意,不能在两端都使用外键映射为1-1,例如下面这种情况,表department表和manager都分别设置了外键manager_id和department_id,那么当一条manager记录单向关联了一条department记录,而这条department记录却关联向另一条manager记录,就会出现问题,如下图所示:
  image_1b372rtct32gnaf1uu216bkcs94a.png-92kB        

基于主键的一对一关联关系

  基于主键的1-1映射策略,是指一端的主键生成器使用foreign策略,表明根据“对方”的主键来生成自己的主键,自己并不能独立生成主键。<param>子节点指定使用当前持久化类的哪一个属性来作为“对方”。例如:

1
2
3
4
<generator class="foreign">
<!-- property 属性指定使用当前持久化类的哪一个属性的主键作为外键 -->
<param name="property">mgr</param>
</generator>

  采用foreign主键生成器策略的一端使用one-to-one元素映射关联属性,需要在one-to-one节点中设置constrained=”true”,以指定为当前持久化类对应的数据库表的主键添加一个外键约束,引用被关联的对象(即“对方”)所对应的数据库表的主键。另一端同样使用one-to-one节点映射关联关系。
  下面我们仍以department和manager的例子进行测试,首先新建两个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Department {
private Integer deptId;
private String deptName;
private Manager mgr;
//getters and setters
}
public class Manager {
private Integer mgrId;
private String mgrName;
private Department dept;
//getters and setters
}

映射文件:
 
Department.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.one2one.primary">
<class name="Department" table="DEPARTMENTS">
<id name="deptId" type="java.lang.Integer">
<column name="DEPT_ID" />
<!-- 使用外键的方式来生成当前的主键 -->
<generator class="foreign">
<!-- property 属性指定使用当前持久化类的哪一个属性的主键作为外键 -->
<param name="property">mgr</param>
</generator>
</id>
<property name="deptName" type="java.lang.String">
<column name="DEPT_NAME" />
</property>
<!--
采用 foreign 主键生成器策略的一端增加 one-to-one 元素映射关联属性,
其 one-to-one 节点还应增加 constrained=true 属性, 以使当前的主键上添加外键约束
-->
<one-to-one name="mgr" class="Manager" constrained="true"></one-to-one>
</class>
</hibernate-mapping>

Manager.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
<?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.one2one.primary.Manager" table="MANAGERS">
<id name="mgrId" type="java.lang.Integer">
<column name="MGR_ID" />
<generator class="native" />
</id>
<property name="mgrName" type="java.lang.String">
<column name="MGR_NAME" />
</property>
<one-to-one name="dept"
class="com.atguigu.hibernate.one2one.primary.Department"></one-to-one>
</class>
</hibernate-mapping>

生成的数据库表如下:
 
managers
image_1b373jjnq7vj1dck1j6f4qkvjg4n.png-16.2kB

departments
image_1b373km8b96o52engkc4s197a54.png-16.9kB
image_1b373l1mp1cm2161gs14amffm5h.png-17.9kB
可以看到,表departments是根据主键DEPT_ID来关联表managers的。
 
下面测试save和get方法:

基于主键的1-1关联关系的save操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void testSave(){
Department department = new Department();
department.setDeptName("DEPT-AA");
Manager manager = new Manager();
manager.setMgrName("MGR-AA");
//设定关联关系
manager.setDept(department);
department.setMgr(manager);
//保存操作
//先插入哪一个都不会有多余的 UPDATE
session.save(department);
session.save(manager);
}

  和之前不同的是,不论是先执行session.save(department);,还是先执行session.save(manager);,效果都是一样的,都只有两条insert语句,不会有update语句,而且都会先执行insert into managers,后执行insert into departments,如下图:
  image_1b374o1mk1nr6pit19rc154h1ais5u.png-22.1kB
  
  这是因为,现在department是根据主键关联manager,主键是不能像外键那样先被置为null然后进行update修改的,所以不论哪一个语句放在前面,都会先等到manager记录插入后,再插入department记录。
  
基于主键的1-1关联关系的get操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void testGet(){
//1. 默认情况下对关联属性使用懒加载
Department dept = (Department) session.get(Department.class, 1);
System.out.println(dept.getDeptName());
//2. 所以会出现懒加载异常的问题.
Manager mgr = dept.getMgr();
System.out.println(mgr.getMgrName());
}
@Test
public void testGet2(){
//在查询没有外键的实体对象时, 使用的左外连接查询, 一并查询出其关联的对象
//并已经进行初始化.
Manager mgr = (Manager) session.get(Manager.class, 1);
/*System.out.println(mgr.getMgrName());
System.out.println(mgr.getDept().getDeptName()); */
}

  基于主键的1-1和基于外键的1-1十分相似,在查询department时都使用懒加载机制,可能会抛出懒加载异常,在查询manager时都会使用左外连接,但不同的是,我们在Manager.hbm.xml文件的one-to-one节点中没有设置property-ref属性,即默认department中关联manager的字段是department的id,这正是我们在基于主键的1-1关系中希望的,所以可以看到,左外连接的连接条件是dept.dept_id = mgr.manager_id:
  image_1b376sjn8145n11rm1844dfshjg6r.png-39.6kB