Hibernate 一对一关联映射

1. 前言

本节课程和大家一起聊聊关联映射。通过本节课程的学习,你将了解到:

  • 什么是关联映射;
  • 如何实现一对一关联映射。

2. 关联映射

关系数据库中的数据以表为家,一张表一个家,一个家住一类数据。众多表组成关系型社区,关系型社区群体中的数据关系通过主外键方式描述

表与表之间的数据根据彼此的关系可分为:

  • 一对一关系: 如老公表和老婆表的关系;
  • 一对多关系: 如用户表和银行账号表关系;
  • 多对一关系: 如银行帐号表对用户表关系;
  • 多对多关系: 如学生表和课程表关系。

不管是哪种关系,都可以通过主外键方式联系。
一对多、多对一本质一样,正如一块硬币的正反面,看待同一个事物的角度不同。
多对多通过中间表的方式拆分成两个一对多(多对一)。

以上都是关系型数据库中的基础知识,美好的回忆有助于巩固。

开发者使用 Hibernate 操作某一张表中的数据时,有 2 件事情要做:

  • 在程序中构建一个与表结构相似的类(这个类可称为实体 [entity] 类);
  • 使用注解或 XML 语法把类结构表结构关联映射起来(此时这个类可称为 PO)。

有了 POHibernate 就能在程序和数据库之间进行数据贸易往来。

2.1 新的需求总是接踵而至

如果程序要求 Hibernate 操作多张表中的数据,并要求把获取的数据封装到对象中,又如何操作?

程序说:亲爱的 Hibernate,帮我查询一下学生信息和学生的家庭地址信息。

差点忘记告诉你:学生信息学生的家庭地址信息分别存放在两张表中,且一名学生只能有一个地址。

这是假设,所以请不要纠结这种假设。

Hibernate 说: 可以。但有几件事情必须按要求做好,否则干不了。

  1. 首先须确定数据库中存在学生表和地址表,并确定两者关系。使用地址编号 addressId 字段作共同字段,addressId 在地址表中是主键,在学生表中是外键。

图片描述

  1. 回到程序,构建两个类:Student 类、Address 类,添加注解描述。分别映射学生表、地址表。
    Hibernate 说:还不够。

数据库中的表之间是有关系的,这种关系在 Student 类Address 类 中也必须体现出来。
这也是 Hibernate 能查询到多表中数据的核心要求。

2.2 PO 之间映射表之间的关系

从编码层面上讲,就是如何在 Student 类Address 类 之间体现出数据库表中数据之间的关系

先从 Student 类 中开始,在 Student 类 中添加一个属性字段。

private Address address;

address 属性是一个 Address 类类型,数据库不认得这玩意儿,Hibernate 表示开始要一个头两个大了,眩晕啦。

null 你先进入学生表,然后根据学生表中的 addressId 进入到地址表,找到对应数据。

Hibernate 又没有读心术,它如何知道你心里所想。

所以,需要通过 XML注解语法把开发者的想法告诉 Hibernate

private Address address;
@OneToOne(targetEntity = Address.class)
@JoinColumn(name = "addressId")
public Address getAddress() {
return address;
}
  • @OneToOne 注解告诉 Hibernateaddress 属性的值要麻烦您先找到 学生表 addressId,再辛苦去一下 地址表,把对应的地址信息查询出来;

  • @JoinColumn:告诉 Hibernate 带着 addressId 到地址表中查找。

主配置文件中添加如下信息:

<property name=*"hbm2ddl.auto"*>create</property>
<mapping class=*"com.mk.po.Student"* />
<mapping class=*"com.mk.po.Address"* />

Hibernate 创建完毕后,添加几条测试数据,操作完成后别忘记改回来。

此处操作自动完成!相信聪明如你,一定没问题。

<property name="hbm2ddl.auto">update</property>

图片描述

2.3 测试时间

查询学生及学生的地址信息:

try {
    transaction = session.beginTransaction();
    Student stu = (Student) session.get(Student.class, new Integer(1));
    System.out.println("----------------学生信息---------------");
    System.out.println("学生姓名:" + stu.getStuName());
    System.out.println("-----------------地址信息-----------------");
    System.out.println("学生家庭地址:"+stu.getAddress().getAddressName());
    transaction.commit();
} catch (Exception e) {
    transaction.rollback();
} finally {
    session.close();
}

输出结果:

Hibernate: 
    select
        student0_.stuId as stuId1_1_1_,
        student0_.addressId as addressI6_1_1_,
        student0_.stuName as stuName2_1_1_,
        student0_.stuPassword as stuPassw3_1_1_,
        student0_.stuPic as stuPic4_1_1_,
        student0_.stuSex as stuSex5_1_1_,
        address1_.addressId as addressI1_0_0_,
        address1_.addressName as addressN2_0_0_,
        address1_.descript as descript3_0_0_ 
    from
        Student student0_ 
    left outer join
        Address address1_ 
            on student0_.addressId=address1_.addressId 
    where
        student0_.stuId=?
----------------学生信息---------------
学生姓名:Hibernate老大
-----------------地址信息-----------------
学生家庭地址:北京

Hibernate 使用 left outer join 构 建一条 Sql 语句,一次性访问 2 张表,同时获取了学生信息和地址信息;

请问 Hibernate,你能不能查询地址信息时,查询出地址是哪个学生的

可以,但是,需要在地址类中添加如下代码:

private Student student;
@OneToOne(targetEntity=Student.class,mappedBy="address")
public Student getStudent() {
	return student;
}
  • @OneToOne 注解的 targetEntity=Student.class 告诉 Hibernate,此属性的值对应的是学生表中的数据;

  • Student 类已经使用了 @OneToOne 映射。mappedBy=“address” 意思是说:Hibernatestudent 类中已经说明的够清楚了吧,这里就不要我再啰嗦了。

测试实例:

try {
    transaction = session.beginTransaction();
	Address address = (Address) session.get(Address.class, new Integer(1));
	System.out.println("----------------地址信息---------------");
	System.out.println("地址信息:" + address.getAddressName());
	System.out.println("-----------------学生信息-----------------");
	System.out.println("学生姓名:" + address.getStudent().getStuName());
	transaction.commit();
} catch (Exception e) {
	transaction.rollback();
} finally {
	session.close();
}

输出结果:Hibernate 自动构建了多表查询语句,一次性从数据库获取所有数据。

Hibernate: 
    select
        address0_.addressId as addressI1_0_1_,
        address0_.addressName as addressN2_0_1_,
        address0_.descript as descript3_0_1_,
        student1_.stuId as stuId1_1_0_,
        student1_.addressId as addressI6_1_0_,
        student1_.stuName as stuName2_1_0_,
        student1_.stuPassword as stuPassw3_1_0_,
        student1_.stuPic as stuPic4_1_0_,
        student1_.stuSex as stuSex5_1_0_ 
    from
        Address address0_ 
    left outer join
        Student student1_ 
            on address0_.addressId=student1_.addressId 
    where
        address0_.addressId=?
----------------地址信息---------------
地址信息:北京
-----------------学生信息-----------------
学生姓名:Hibernate老大

无论是从学生表查询到地址表,还是从地址表查询到学生表。只要有足够的信息告诉 Hibernate 如何关联到数据库中对应的表,Hibernate 都会如你所愿。

3. 关联映射中的延迟加载

关联多表查询时可选择是否启用延迟加载。

PO 之间的映射,意味着 Hibernate 不仅能查询到指定表中数据,还能查询相关联表中的数据。

但,有时只需要查询学生基本信息,并不需要地址信息,或者地址信息并不需要马上查询出来,能不能告诉 Hibernate,只查询学生信息,暂且别查询地址信息。

同样,有时只需要查询所在地址,并不关心地址对应学生信息。

可以启动关联映射中的延迟加载实现上面的需求。

学生类中修改代码如下:

@OneToOne(targetEntity = Address.class,fetch=FetchType.LAZY)
@JoinColumn(name = "addressId")
public Address getAddress() {
    return address;
}

@OneToOne 注解有 fetch 属性,为枚举类型,其值可选择:

  • FetchType.LAZY
  • FetchType.EAGER

其作用便是告诉 Hibernate,是否延后或立即查询相关联表中的数据。

执行下面测试代码:

try {
    transaction = session.beginTransaction();
	Student stu = (Student) session.get(Student.class, new Integer(1));
	System.out.println("----------------学生信息---------------");
	System.out.println("学生姓名:" + stu.getStuName());
	transaction.commit();
} catch (Exception e) {
	transaction.rollback();
} finally {
	session.close();
}

查看结果:

Hibernate: 
    select
        student0_.stuId as stuId1_1_0_,
        student0_.addressId as addressI6_1_0_,
        student0_.stuName as stuName2_1_0_,
        student0_.stuPassword as stuPassw3_1_0_,
        student0_.stuPic as stuPic4_1_0_,
        student0_.stuSex as stuSex5_1_0_ 
    from
        Student student0_ 
    where
        student0_.stuId=?
----------------学生信息---------------
学生姓名:Hibernate老大

Hibernate 只构建了一条简单的 Sql 语句, 用于查询学生信息。

继续执行下面测试实例:

try {
	transaction = session.beginTransaction();
    Student stu = (Student) session.get(Student.class, new Integer(1));
	System.out.println("----------------学生信息---------------");
	System.out.println("学生姓名:" + stu.getStuName());
	System.out.println("-----------------地址信息-----------------");
	System.out.println("学生家庭地址:" + stu.getAddress().getAddressName());
	transaction.commit();
} catch (Exception e) {
	transaction.rollback();
} finally {
	session.close();
}

输出结果:

Hibernate: 
    select
        student0_.stuId as stuId1_1_0_,
        student0_.addressId as addressI6_1_0_,
        student0_.stuName as stuName2_1_0_,
        student0_.stuPassword as stuPassw3_1_0_,
        student0_.stuPic as stuPic4_1_0_,
        student0_.stuSex as stuSex5_1_0_ 
    from
        Student student0_ 
    where
        student0_.stuId=?
----------------学生信息---------------
学生姓名:Hibernate老大
-----------------地址信息-----------------
Hibernate: 
    select
        address0_.addressId as addressI1_0_0_,
        address0_.addressName as addressN2_0_0_,
        address0_.descript as descript3_0_0_ 
    from
        Address address0_ 
    where
        address0_.addressId=?
学生家庭地址:北京

Hibernate 分别构建了 2 条简单的查询 Sql 语句,可得出结论:

  • 只有当需要获取地址信息时,才会构建 Sql 语句查询地址表;

这就是关联映射中的延迟加载。

  • @OneToOne 默认情况下是采用立即策略,通过构建多表查询语句一次性全部查询。

4. 小结

本节课讲解了如何通过实现实体类之间的映射,以保证 Hibernate 能正常访问开发者所需要的关联表中的数据。其中有些细节暂未深究。

当介绍一对多、多对多映射关系时,再抽丝剥茧般展开。

OK!意犹未尽之处,下一节课程再畅聊。