Fork me on GitHub

Hibernate框架学习(三)

Hibernate Framework

简介:
Hibernate框架是一个全自动的ORM框架,它拥有强大的映射功能,提供了缓存机制、事务管理、拦截器机制、查询语句的多方面支持

本系列笔记可能会遇到的专业词汇有:

  • Framework, 框架,某一类问题的总体解决方案

  • ORM, Object Relationship Mapping, 对象关系映射

  • DATABASE, 数据库,存储数据的一种方式

  • HQL, Hibernate Query Language, Hibernate查询语句

  • Transaction, 事务,一组相关的操作

  • Session, 会话

本系列笔记包含如下的课程内容:

  • Hibernate框架原理和开发流程
  • 框架缓存机制
  • 对象关系映射
  • 框架提供的查询机制
    • 基于底层的SQL查询机制
    • 基于HQL查询机制
    • 基于Criteria查询机制

Hibernate 关联映射

logo

此章节重点讲解Hibernate框架的关联映射

章节重点:

  • 实体关系
  • 一对多、多对多、一对一关联
  • 加载策略
  • 级联操作

章节难点:

  • 多对多关联映射
  • 延迟加载策略

实体关系

实体关系(Entity Relationship)是用来描述问题领域中实体之间关系的一种表述。在面向对象分析 (OOA)阶段,通常用E-R图来表达实体类之间的关系;然后再在面向对象设计(OOD)阶段,将E-R使用具体语言比如Java、C#等加以实现。

实体之间的关系有:

  • 关联关系 [横向关系]
    • 一对多关联
    • 一对一关联
    • 多对多关联
  • 依赖关系 [横向关系]
  • 继承关系 [纵向关系]
  • 实现关系 [纵向关系]

注:所有的关联关系都有 单向关联和双向关联 两种

注:在Hibernate中,支持关联关系映射和继承关系映射,而依赖可以改写为关联,实现本质上就是继承。

本章节重点讲解关联关系映射,在附录部份我们讲解一下继承关系映射

一对多关联

一对多关联关系是最常见的一种关系,描述为:
类型A的一个对象可以持有类型B的多个对象的引用,同时类型B的任何一个对象最多只能持有A的一个对象

图示

典型的一对多例子:
客户:订单,一个客户可以有多个订单,每个订单只对应一个客户;
部门:员工,一个部门可以有多个员工,每个员工只从属一个部门;
省份:城市,一个省份有多个城市,每个城市只属于一个省份。

在关系型数据库中,通常的做法是:

Many的一方设置FK外键,引用One的主键,作为外键约束;

图示

如果只是一方持有另一方的引用,叫做 单向关联(unidirectional)

单向关联图示

图示

如果双方同时持有对方的引用,叫做 双向关联(Bidirectional)

双向关联图示

图示

代码示例 使用Annotation来描述实体关联映射

部门类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Entity
@Table(name = "TBL_DEPT")
public class Department implements Serializable {
private Integer id; //主键
private String name; //部门名
private Set<Emp> empSet; //与员工的关联,持有多个员工

@Id
@GeneratedValue
public Integer getId() { return this.id; }
@Column
public String getName() { return this.name; }
@OneToMany(mappedBy="dept") //使用 @OneToMany来描述一对多关系
public Set<Emp> getEmpSet() { return this.empSet; }

// set方法省略....
}

员工类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Entity
@Table(name = "TBL_EMP")
public class Emp implements Serializable {
private Integer id; //主键
private String name; //员工名
private Date start_date; //入职日期
private String job; //职位
private Department dept; //与部门的关联,持有单个部门

@Id
@GeneratedValue
public Integer getId() { return this.id; }
@Column
public String getName() { return this.name; }
@Column
@Temporal(TemporalType.DATE)
public Date getStart_date() { return this.start_date; }
@ManyToOne // 使用 @ManyToOne来描述多对一
@JoinColumn(name = "DEPT_ID") //指定外键列
public Department getDept() { return this.dept; }

//set方法省略 ...
}

上面的代码描述的是双向的一对多关联, 需要注意的是:在面向关系的描述中,不管是双向或是单向,在映射到关系型数据库中都是同一种结果,如下图

图示

注: @Column和@JoinColumn 的区别
前者定义实体类的属性,映射到数据库中某一张表的具体字段类型(数字,字符串,日期等),后者定义连接(Join)两个实体类之间的关系,映射到数据库中,就是两张表之间的FK外键。
注意,@JoinColumn 写在哪个实体类中,不代表FK就在这个类映射的表中。 可以查看[API]看案例

思考?
单向关联 or 双向关联?
答:没有固定的配置方式,一切可以按自己的需求进行。

可以通过下图的对比来进行选择
图示

一对一关联

类型A到类型B的一对一关系描述为:
类型A的任何一个对象最多持有类型B的一个对象的引用,
类型B的任何一个对象最多持有类型A的一个对象的引用。

图示

典型的一对一例子:
丈夫: 妻子,一个丈夫对应一个妻子;
班级:班长,一个班级有一个班长;
用户:身份证号,一个客户持有一个身份证号。

注:一对一关系,在关系型数据库中,可以视作一对多关系的一种特例。

表A和表B是一对一关系,实现方式:

  • A表和B表,各自有独立的主键,B表设置 FK 外键字段,引用A表的主键,并增加unique唯一约束。
  • A表有独立主键,B表无独立主键,B表的主键是引用A表的外键。[主外键合一]

一对一的单向关联和双向关联,都只有一种配置方法, 如下图:

图示

代码示例 使用Annotation来描述实体关联映射

Husband类

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
@Entity
@Table(name = "TBL_Husband")
public class Husband implements Serializable {
private Integer id; //主键
private String name; //姓名
private int age; //年龄

private Wife wife; //妻子
@Id
@GeneratedValue
public Integer getId() { return this.id; }
@Column
public String getName() { return this.name; }
@Column
public int getAge() { return this.age; }

/******
* 当前类和Wife属性是一对一关系,Husband类是主表,维护表间关系,Wife是从表,持有Husband的外键
*/
@OneToOne(mappedBy="husband")
public Wife getWife() { return this.wife; }

//setter方法省略
...
}

Wife类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Entity
@Table(name = "TBL_Husband")
public class Wife implements Serializable {
private Integer id; //主键
private String name; //姓名

private Husband husband; //老公
@Id
@GeneratedValue
public Integer getId() { return this.id; }
@Column
public String getName() { return this.name; }

@OneToOne //与Husband的映射
//给生成的外键,自定义属性(字段名、唯一性)
@JoinColumn(name="husband_id", unique=true)
public Husband getHusband() { return this.huband; }

//setter方法省略 ...
}

可以看出,一对一的关联就是一种特殊的一对多,因为在表结构看来,就是在外键的基础上再多一个唯一性约束

多对多关联

类型A到类型B的多对多关系描述为:
类型A的任何一个对象可以持有类型B的多个对象的引用,
类型B的任何一个对象可以持有类型A的多个对象的引用。

图示

典型的一对一例子:
学生:课程,一个学生修多门课,一门课有多个学生修;
演员:角色,一个演员可以演多个角色,一个角色可由不同演员演绎;
用户:角色,一个用户可以分配多个角色,一个角色可以分配给多个用户。


注:
在关系型数据库中,体现多对多关系,必须要配置 中间表该表只具有2个FK字段,分别引用到两个表中的ID主键. 如下图

图示

多对多的单向关联和双向关联,都只有一种配置方法,如下图:

图示

多对多代码示例 使用Annotation来描述实体关联映射

Role类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Entity
@Table(name="TBL_Role")
public class Role implements Serializable {
private Integer id; //主键
private String name; //角色名

private Set<User> users; //用户集合

@Id
@GeneratedValue
public Integer getId() { return this.id; }
@Column
public String getName() { return this.name; }
/*****
* 此处 Role是主表,User是子表,所以,在Role类中,只需要写ManyToMany即可,另一边描述关系
*/
@ManyToMany(mappedBy="roles")
public Set<User> getUsers() { return this.users; }

//setter方法省略 ...
}

User类

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
@Entity
@Table(name="TBL_User")
public class User implements Serializable {
private Integer id; //主键
private String name; //用户名
... //其它的属性

private Set<Role> roles; //角色集合

@Id
@GeneratedValue
public Integer getId() { return this.id; }
@Column
public String getName() { return this.name; }
/*****
* User是子表,这里通过@JoinTable来定义中间表
*/
@ManyToMany
@JoinTable(name="tbl_user_to_role",
joinColumns={@JoinColumn(name="user_id")}, //关系由谁维护,谁的外键写在此
inverseJoinColumns={@JoinColumn(name="role_id")} //关系的另一边写在此
)
public Set<User> getUsers() { return this.users; }

//setter方法省略 ...
}

注,多对多关系在映射时无论在哪边来描述关系都是可以的。

相关的注解

  • [@OneToMany]
  • [@ManyToOne]
  • [@OneToOne]
  • [@ManyToMany]
  • [@JoinColumn]
  • [@JoinTable]

操作原则

我们通过代码操作关联关系时,应该遵守如下三个原则:

  1. 原则一
    • 在关系型数据库中,如需体现记录和记录之间的对应关系,则需在OO层面也体现对象和对象之间的对应关系。
  2. 原则二
    • 如果是双向关联,类型A与类型B互相持有对象的引用,则使用子表对象set主表对象。
  3. 原则三
    • 从无到有,先主后子;
    • 从有到无,先子后主。

加载策略

在Hibernate框架中,加载数据有两种策略,如下:

  1. 懒汉模式策略, 也叫 Lazy模式
    • 不确定资源是否被程序调用,为了避免资源浪费,等需要使用时,再去初始化资源;
  2. 饿汉模式策略, 也叫 Eager模式
    • 为了让使用者不要等待,一开始就初始化资源。尽量保证被初始化的 资源会被使用到.

Hibernate框架根据不同的情况,自动选择采用哪种策略加载数据,当然,也可以手动强制配置

Hibernate 中的懒加载机制:

对于实体类A的查询操作,如果A的属性中,存在实体类B的集合,默认对实体类B的集合使用延迟加载策略(懒汉模式)。可能发生懒加载的情况:一对多,多对多。
如果A的属性中,存在实体类C的单个对象,默认对实体类C的使用饿汉模式。

注:
如果在使用多的一方的数据之前,session已被close,则会抛错:

org.hibernate.LazyInitializationException: failed to lazily initialize a collection of xxx

以上是默认的情况,如果你在@OneToMany中,指定了 fetch=FetchType.EAGER, 则表示针对当前的实体对象加载时,会强制加载多的一边。

反过来,如果你在 @ManyToOne中,指定了 fetch=FetchType.LAZY, 则表示针对当前的实体对象加载时,对于
关联的一的一边,也将采用延迟加载

以上的两种做法,都是改变了默认的行为,除非你确定要这么做,否则,不建议修改默认行为。

级联操作

所谓级联操作,定义对某个实体类对象进行某种操作(CRUD)时,是否对其关联的对象也做类似操作。
目的:简化存在关联关系的实体类操作的开发效率。

  • 级联操作是可选的,不是必须的。
  • 在主表对象和子表对象,在某个或全部操作(CUD)上,生命周期都一致时,才使用级联。
  • 级联操作,一般使用在OneToOne,OneToMany的操作上,ManyToMany通常不设置。
  • 以上情况之外,完全不需要设置级联。

在@OneToOne,@OneToMany, @ManyToOne中,都可以通过属性cascade来指定级联操作类型。

Hibernate框架中定义了很多的级联操作类型,如:

假设存在如下的A类和B类关系:

1
2
3
4
5
6
7
8
class A {
...
private B b;

}
class B {
....
}

则,定义在A类发生什么变化的时候,B类做出级联操作(指新增、更新、删除)
图示

级联操作代码示例

Department类的代码片断

1
2
3
4
5
6
7
...
@OneToMany(mappedBy="dept", cascade=CascadeType.REMOVE)
/******
* 当前类与Emp是1对多的关系
* Department 是主表,Emp 是从表
*/
public Set<Emp> getEmps() { return this.emps; }

没有指定级联删除的情况

1
2
3
4
5
6
7
8
9
//获取部门
int id = 1;
Department dept = (Department)session.get(Department.class,id);
//如果没有指定级联操作的话,则需要先迭代把此部门下的所有员工删除先
for(Emp e : dept.getEmps()) {
session.delete(e);
}
//最后才去删除 dept本身,【因为存在子记录】
session.delete(dept);

指定了级联删除的情况

1
2
3
4
5
//而如果指定了级联删除的话,则可以直接删除 dept, 如下
int id = 1;
Department dept = (Department)session.get(Department.class,id);
//删除部门对象,级联删除子对象
session.delete(dept);

常见问题

  • 常见问题1

MappingException: Could not determine type for:
com.tz.hibernate.entity.Department, at table: emp, for columns: [org.hibernate.mapping.Column(dept)]

原因:
属性类型是自定义实体类,不是基础数据类型,hibernate无法自动产生表的字段类型;
解决方案:
使用合适的Annotation,来表示当前类和该属性的类型, 之间的关联关系.
步骤:
先找到该实体类属性的get方法;加上合适的Annotation,比如: @ManyToOne, 如下图:
图示

  • 常见问题2

现象:
在配置双向关联的情况下,自动产生的表结构会发生异常(1:N,N:N 会冗余一张表,1:1紧耦合)。

问题原因:
这种配置情况下, A和B是对等关系,hibernate无法得知哪个对象是主表对象。
解决方案:
通过配置,告知Hibernate,哪个对象是主表对象.
步骤:

  • 先确定哪个是主表对象?
    • 在一对多关系中,1的一方是主表,N的一方是子表.
    • 在1:1,N:N关系中,由于是对等关系, 由开发者视情况而定.
  • 进入主表对象源代码,找到之前配置的那个@关联关系, 新增mappedBy属性; 值就写get方法返回类型中,看哪个属性类型是主表对象.
  • 值写的是属性名, 不是类型.
  • 常见问题3

现象:
调用Session的save()方法,保存1:1, 1:N, N:N 的数据,能成功保存。但数据库中,FK外键没有值。

问题原因:
没有遵守原则一:
要在关系型数据库中体现出记录之间的对应关系;
要在面向对象的层面,也体现出对象之间的对应关系.
解决方案:
保存对象之前,调用子表对象 set 主表对象,来指出对象之间的对应关系。

  • 常见问题4

问题:
在一对多,或多对多的查询操作时,报错:org.hibernate.LazyInitializationException: failed to lazily initialize a collection of xxx

原因:
在使用延迟加载的数据之前,session已被close。
解决方案:

- 在session关闭之前,调用延迟加载的数据,完成加载;  
- 使用Query/Criteria接口,在查询级别上使用关联查询,避免延迟加载。  

本章小节

  • 掌握OneToMany,OneToOne,ManyToMany的配置和常用CRUD操作
  • 了解延迟加载的原理和相关问题
  • 了解级联操作的目的和要点

参考API

@JoinColumn
@OneToMany
@ManyToOne
@OneToOne
@ManyToMany
@JoinColumn
@JoinTable