SSH Chapter 06 Hibernate 关联映射 笔记
本章目标:
- 理解Hibernate的关联映射
- 理解inverse属性、cascade属性
- 掌握单向的多对一、双向的一对多映射
- 掌握多对多映射
- 掌握延迟加载
技术内容:
前面我们已经掌握了如何配置Hibernate
对数据库进行增
,删
,改
,查
操作,掌握Java对象的生命周期.
在学习面向对象时,学习过对象之间存在关联的关系.
在学习数据库时,也学习过表和表之间也可以通过外键关联起来.
怎样映射面向对象领域的关联关系和数据库关系中的外键关联呢?
这是本章要关注的问题.
1 . 关联关系
类与类之间最普遍的关系就是关联关系,而且关联是有方向的。
**以部门(Dept
)和员工(Emp
)为例,一个部门下有多个员工,而一个员工只能属于一个部门. **
从
Emp
到Dept
的关联是多对一关联,这就意味着每个Emp
对象只能引用一个Dept
对象;
**而从
Dept
到Emp
是一对多关联,这意味着每个Dept
对象会引用一组Emp
对象; **
因此,在
Emp
类中应该定义一个Dept
类型的属性,来引用所关联的Dept
对象;而在Dept
类中应该定义一个集合类型的属性,来引用所有关联的Emp
对象
如果仅有从Emp
到Dept
的关联,或者仅有从Dept
到Emp
的关联,就称为单向关联.如果同时包含两种关联,就称为双向关联,如图:
关联关系是使用最多的一种关系,非常重要。在内存中反映为实体关系,映射到DB中为主外键关系。
实体间的关联,即对外键的维护。关联关系的发生,即对外键数据的改变。
外键:外面的主键,即,使用其它表的主键值作为自已的某字段的取值。
本章将结合具体案例来介绍如何映射以下关联关系:
(1)以
Emp
类和Dept
类为例 , 介绍如何映射多对一单向
关联关系(2)以
Emp
类和Dept
类为例 , 介绍如何映射一对多双向
关联关系(3)以
Project
类和Employee
类为例 , 介绍如何映射多对多
关联关系
2 . 建立单向多对一关联
首先以Emp
和Dept
为例介绍如何建立单向多对一关联关系.
2.1 配置单向多对一关联
在Emp
类中需要定义一个Dept
属性,而在Dept
类中无须定义用于存放Emp
对象的集合属性,示例1和示例2分别是Dept
和Emp
持久化类.
示例1:
Dept
持久化类的主要内容:
/**
* 部门信息
*/
public class Dept implements Serializable {
//private Byte deptNo;
private Short deptNo;
private String deptName;
private String location;
//此处省略无参构造,省略toString(),以及属性的getter和setter
}
示例2:
Emp
持久化类的主要内容:
/**
* 雇员类
*/
public class Emp implements Serializable {
private Integer empNo;
private String empName;
private String job;
private Double salary;
private Date hireDate;
private Dept dept;
//此处省略无参构造,省略toString(),以及属性的getter和setter
}
Dept
类的所有属性和DEPT
表的字段一一对应,因此把Dept
类映射到DEPT
表非常简单,如示例3所示:
示例3:
Dept.hbm.xml
的主要内容:
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="cn.hibernatedemo.entity">
<class name="Dept" table="DEPT" >
<id name="deptNo" column="DEPTNO">
<generator class="assigned"/>
<!--<generator class="increment"/>-->
</id>
<property name="deptName" column="DNAME"/>
<property name="location" column="LOC"/>
</class>
</hibernate-mapping>
而在Emp
类中,其dept
属性是Dept
类型 , 和EMP
表的外键DEPTNO
对应,而EMP
表的外键DEPTNO
是数值类型,显然类型不匹配.属性dept
代表了Emp
类对Dept
类的管理关系 , 不能使用<property>
元素来映射,而要使用<many-to-one>
元素,如示例4所示:
示例4:
Emp.hbm.xml
的主要内容:
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="cn.hibernatedemo.entity">
<class name="Emp" table="EMP">
<id name="empNo" column="EMPNO">
<!--<generator class="assigned"/>-->
<generator class="increment"/>
</id>
<property name="empName" column="ENAME"/>
<property name="job" column="JOB"/>
<property name="salary" column="SAL"/>
<property name="hireDate" column="HiREDATE"/>
<!--many-to-one元素建立了EMP表的外键DEPTNO和dept属性之间的映射.
name:设定持久化类的属性名,此处为Emp类的dept属性.
column:设定持久化类的属性对应的表的外键,此处为EMP表的外键DEPTNO
class:设定持久化类的属性的类型,此处设定dept属性为Dept类型-->
<many-to-one name="dept" column="DEPTNO" class="Dept"/>
</class>
</hibernate-mapping>
<many-to-one>
元素建立了EMP
表的外键DEPTNO
和dept
属性之间的映射.它包括以下属性:
(1)
name
: 设定持久化类的属性名,此处为Emp
类的dept
属性.(2)
column
: 设定持久化类的属性对应的表的外键,此处为EMP
表的外键DEPTNO
(3)
class
: 设定持久化类的属性的类型,此处设定dept
属性为Dept
类型至此 ,
Emp
类到Dept
类的单向多对一映射就完成了.
2.2 实现持久化操作
Dept
类,Emp
类及其映射文件已经编写完成 , 从Emp
到Dept
的单向多对一关联已经建立.在这个基础上,实现具有关联关系的对象的持久化.
(1) . 添加或修改Emp
对象
外键信息封装在Dept
对象中 , 需建立Emp
对象和Dept
对象的关联 , 最后保存或更新Emp
对象,如示例5
所示:
示例5:
EmpDao.java
关键代码如下:
public void save(Emp emp){
this.currentSession().save(emp);
}
EmpBiz.java
关键代码如下:
public void addNewEmp(Emp emp){
Transaction transaction = null;
try {
transaction = empDao.currentSession().beginTransaction();
empDao.save(emp);
transaction.commit();//提交事务
} catch (Exception e) {
e.printStackTrace();
HibernateUtil.rollback(transaction);
}
}
测试方法关键代码如下:
@Test
public void testAddNewEmp() throws Exception {
Emp emp = new Emp();
emp.setEmpName("张三");
//指定员工所在的部门为会计部门
Dept dept = new Dept();
dept.setDeptNo((short)10);//会计部门的编号
emp.setDept(dept);
EmpBiz empBiz = new EmpBiz();
//保存雇员数据
empBiz.addNewEmp(emp);
}
运行示例5 , Hibernate
执行以下insert
语句:
Hibernate:
select
max(EMPNO)
from
EMP
Hibernate:
select
dept_.DEPTNO,
dept_.DNAME as DNAME2_0_,
dept_.LOC as LOC3_0_
from
DEPT dept_
where
dept_.DEPTNO=?
Hibernate:
insert
into
EMP
(ENAME, JOB, SAL, HiREDATE, DEPTNO, EMPNO)
values
(?, ?, ?, ?, ?, ?)
(2). 根据Dept
查询相关的Emp
对象
按照指定的Dept
对象来查询相关的Emp
对象,如示例6
所示:
注意:测试对象间导航效果,需在会话关闭前测试查询效果
示例6:
EmpDao.java
中的关键代码如下:
public List<Emp> findByDept(Dept dept){
return this.currentSession().createQuery("from Emp where dept=:dept").setParameter("dept",dept).list();
}
EmpBiz.java
中的关键代码如下:
public List<Emp> findEmpsByDept(Dept dept) {
Transaction transaction = null;
List<Emp> list = null;
try {
transaction = empDao.currentSession().beginTransaction();
list = empDao.findByDept(dept);
//注意:测试对象间导航效果,注意需在会话关闭前测试查询效果
//原因会在下文的延迟加载中进行分析
list.forEach(System.out::println);
transaction.commit();//提交事务
} catch (Exception e) {
e.printStackTrace();
HibernateUtil.rollback(transaction);
}
return list;
}
测试方法中的关键代码如下:
@Test
public void testFindEmpsByDept() throws Exception{
Dept dept = new Dept();
dept.setDeptNo((short)10);
EmpBiz empBiz = new EmpBiz();
List<Emp> list = empBiz.findEmpsByDept(dept);
}
运行示例6时,Hibernate
会执行以下select
语句:
Hibernate:
select
emp0_.EMPNO as EMPNO1_1_,
emp0_.ENAME as ENAME2_1_,
emp0_.JOB as JOB3_1_,
emp0_.SAL as SAL4_1_,
emp0_.HiREDATE as HiREDATE5_1_,
emp0_.DEPTNO as DEPTNO6_1_
from
EMP emp0_
where
emp0_.DEPTNO=?
或者也可以使用如from Emp where dept.deptNo=:deptNo
的HQL
语句实现,直接传递部门编号作为查询条件
(3) 查询Emp
关联的Dept
信息
输出指定Emp
集合中所有Emp
对象及其关联的Dept
对象的信息.
由于
Emp
和Dept
之间存在着单向多对一关系 , 所以只要调用emp.getDept()
方法,就可以方便的从Emp
对象导航到Dept
对象,如示例7所示:
示例7:
EmpDao.java
中的关键代码如下:
public List<Emp> findAllEmp() {
return this.currentSession().createQuery("from Emp").list();
}
EmpBiz.java
中的关键代码如下:
public List<Emp> findAllEmp() {
Transaction transaction = null;
List<Emp> list = null;
try {
transaction = empDao.currentSession().beginTransaction();
list = empDao.findAllEmp();
list.forEach(System.out::println);
transaction.commit();//提交事务
} catch (Exception e) {
e.printStackTrace();
HibernateUtil.rollback(transaction);
}
return list;
}
测试方法中的关键代码如下:
@Test
public void testFindEmps() throws Exception {
EmpBiz empBiz = new EmpBiz();
List<Emp> list = empBiz.findAllEmp();
}
运行示例7,Hibernate执行以下查询语句:
Hibernate:
select
emp0_.EMPNO as EMPNO1_1_,
emp0_.ENAME as ENAME2_1_,
emp0_.JOB as JOB3_1_,
emp0_.SAL as SAL4_1_,
emp0_.HiREDATE as HiREDATE5_1_,
emp0_.DEPTNO as DEPTNO6_1_
from
EMP emp0_
Hibernate:
select
dept0_.DEPTNO as DEPTNO1_0_0_,
dept0_.DNAME as DNAME2_0_0_,
dept0_.LOC as LOC3_0_0_
from
DEPT dept0_
where
dept0_.DEPTNO=?
3 . 建立双向一对多关联关系
当类与类之间建立了关联 , 就可以方便的从一个对象导航到两一个对象 , 或者通过集合导航到一组对象 .
例如 , 对于给定的Emp
对象 , 如果想获得与它关联的Dept
对象 , 只要调用如下方法:
Dept dept = emp.getDept();//从Emp对象导航到关联的Dept对象
对于给定的Dept
对象 , 如果想要获得与它关联的所有Emp
对象 , 该如何处理呢?
在之前讲解的单向多对一映射中 , 由于Dept
对象不和Emp
对象关联,所以必须通过Hibernate
API 查询数据库:
String hql = "from Emp where dept.deptNo = 10";
List<Emp> list = session.createQuery(hql).list();
在使用面向对象语言编写的程序中 , 通过关联关系从一个对象导航到另一个对象显然比通过编码到数据库查询来得更加自然 , 且无须额外的编码 .
并且 , 基于关联关系 , 在
增
,删
,改
操作中还可以对相关对象实现自动化的==级联处理== , 同时减少了编码工作量 , 提高了开发效率 . 因此不妨为Dept
类和Emp
类建立双向一对多关联.
3.1 配置双向一对多关联
在前面的示例中已经建立了Emp
类到Dept
类的多对一关联 , 下面再增加Dept
到Emp
类的一对多关联 , Dept
类和Emp
类之间就构成了双向的关联 , 即双向一对多关联 .
这需要在Dept
类中增加一个集合类型的emps
属性:
//private Set<Employee> emps;//部门对应多个员工,即一对多的关系
private Set<Employee> emps = new HashSet<>();//方便赋值,这里可以直接创建实例化
//省略getter 和 setter
有了以上属性 , 对于给定的部门 , 查询该部门下的所有员工 , 只需要调用 dept.getEmps()
方法即可 . Hibernate
要求在持久化类中定义集合类型属性时 , 必须把属性声明为接口类型 , 如java.util.Set
, 可以指定泛型java.util.Set<Emp>
.
在定义emps
集合时 , 通常把它初始化为集合实现类的一个实例 , 例如:
private Set<Employee> emps = new HashSet<>();//方便赋值,这里可以直接创建实例化
如果不将集合emps
初始化 , 那么在每个调用getEmps()
方法的地方都需要判断是否为null
,降低了程序的可读性 ; 如果疏漏了null
值的判断,就可能会遇到NullPointException
,损害了程序的健壮性.
示例8展示了双向关联时
Dept
类的主要内容:
示例8:
Dept.java
关键代码如下:
/**
* 部门信息
*/
public class Dept implements Serializable {
//private Byte deptNo;
private Short deptNo;
private String deptName;
private String location;
//private Set<Emp> emps;//部门对应多个员工,即一对多的关系
private Set<Emp> emps = new HashSet<>();
//方便赋值,这里可以直接创建实例化
//省略属性的getter和setter方法 省略toString()
}
接下来的问题是如何在映射文件中映射集合类型的emps
属性 . 由于在DEPT
表中没有直接与emps
属性对应的字段 , 所以不能用<property>
元素来映射emps
属性 , 而是使用<Set>
元素 , 示例9
中展示了Dept.hbm.xml
主要内容 . Emp.hbm.xml
的内容和之前示例中相同.
示例9:
Dept.hbm.xml
主要内容如下:
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="cn.hibernatedemo.entity">
<!--<class name="Dept" table="DEPT" dynamic-update="true">-->
<class name="Dept" table="DEPT">
<id name="deptNo" column="DEPTNO">
<generator class="assigned"/>
<!--<generator class="increment"/>-->
</id>
<property name="deptName" column="DNAME"/>
<property name="location" column="LOC"/>
<!-- name指定了映射的集合的属性,即集合实例化的emps;
table指定了集合属性对应的集合表 -->
<set name="emps" table="EMP">
<!--column指定了集合表的外键列 -->
<key column="DEPTNO"></key>
<!-- class由于上面已经写了包名,这里直接使用即可 -->
<one-to-many class="Emp"/>
</set>
</class>
</hibernate-mapping>
<set>
元素的name
属性 : 设定持久化类的属性名 , 此处为Dept
类的emps
属性
<set>
元素的table
属性 : 设定了集合属性对应的集合表 , 此处为 EMP
表
<set>
元素还包含两个子元素:
<key>
元素 : column属性设定与所关联的持久化类相对应的表的外键 , 此处为EMP
表的DEPTNO
字段
<one-to-many>
元素 : class属性设定所关联的持久化类型 , 此处为Emp
类Hibernate
通过以上的映射代码 , 可获得一下信息:
<set>
元素表明Dept
类的emps
属性为java.util.Set
集合类型, 同时该属性对应的表为EMP
表
<one-to-many>
子元素表明emps
集合中存放的是一组Emp
对象.
<key>
子元素表明EMP
表通过外键列DEPTNO
参照DEPT
表
案例需求:增加部门信息的时候 同时设置一个雇员的部门编号为该部门
演示如下:
DeptDao.java
代码如下:
public class DeptDao extends BaseDao {
//增加部门信息的时候 同时设置一个雇员的部门编号为该部门
public void save(Dept dept){
this.getCurrentSession().save(dept);
}
}
DeptBiz.java
代码如下:
public class DeptBiz {
DeptDao deptDao = new DeptDao();
/**
* 新增一个部门 并指定张三的部门为该部门
* @param dept
*/
public void addNewDept(Dept dept){
Transaction tx = null;
try {
tx = deptDao.getCurrentSession().beginTransaction();
deptDao.save(dept);
tx.commit();
} catch (Exception e) {
e.printStackTrace();
HibernateUtil.rollback(tx);
}
}
}
测试代码如下:
@Test
public void test_01(){
DeptBiz deptBiz = new DeptBiz();
Dept dept = new Dept();
dept.setDname("开发部");
dept.setLoc("南区");
Emp emp = new Emp();
//张三的雇员编号
emp.setEmpno(7935L);
//建立单向一对多的关联关系
//也就是建立 部门到雇员之间的关系
//把雇员信息加入到部门的emps属性的集合里
dept.getEmps().add(emp);
deptBiz.addNewDept(dept);
}
3.2 双向关联关系下的增删改操作
如前文所述 , 基于关联关系处理可以通过对象间导航实现相关对象的自动检索外 , 还可以在对象增删改操作中,对相关对象实现自动化的级联操作处理 , 而无需进行相关编码 , 从而减少了编码工作量 , 提高了开发效率 .
级联操作的细节可以在持久化类的映射文件中通过
cascade
属性 和inverse
属性进行控制 :
1. cascade
属性 :
Dept
类 , Emp
类及其映射文件已经编写完成 , 从Dept
到Emp
的双向一对多关联已经建立 , 下面来完成以下持久化操作:
- 先创建
Dept
对象 , 然后创建一个Emp
对象 , 将这两个对象进行关联 . 最后保存Dept
对象 , 同时自动保存这个Emp
对象.- 删除
Dept
对象 , 并级联删除与Dept
对象关联的Emp
对象
要完成以上两个持久化操作 , 需要在<set>
元素中配置cascade
属性.
在对应-关系映射文件中 , 用于映射持久化类之间关联关系的元素 , 如
<set>
,<many-to-one>
都有一个cascade
属性 . 它用于指定如何操纵与当前对象关联的其他对象 .
如下表列出了cascade
属性的部分常用可选值:
cascade属性值 | 描 述 |
---|---|
none | 当Session 操纵当前对象时,忽略其他关联的对象。它是cascade 属性的默认值 |
save-update | 当通过Session 的save() 、update() 及saveOrUpdate() 方法来保存或更新当前对象时,级联保存所有关联的新建的瞬时状态的对象,并且级联更新所有关联的游离状态的对象 |
merge | 当通过Session 的merge() 方法来保存或更新当前对象时,对其关联对象也执行merge() 方法 |
delete | 当通过Session 的delete() 方法删除当前对象时,会级联删除所有关联的对象 |
all | 包含所有的级联行为 |
需求说明 : 完成第一个持久化操作 , 保存Dept
对象的同时级联保存与Dept
对象关联的Emp
对象,如示例10所示:
示例10:
修改Dept.hbm.xml
, 在<set>
元素中设置cascade
属性:
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="cn.hibernatedemo.entity">
<!--<class name="Dept" table="DEPT" dynamic-update="true">-->
<class name="Dept" table="DEPT">
<id name="deptNo" column="DEPTNO">
<generator class="assigned"/>
<!--<generator class="increment"/>-->
</id>
<property name="deptName" column="DNAME"/>
<property name="location" column="LOC"/>
<!-- name指定了映射的集合的属性,即集合实例化的emps;table指定了集合属性对应的集合表 -->
<set name="emps" table="EMP" cascade="save-update">
<!--column指定了集合表的外键 -->
<key column="DEPTNO"></key>
<!-- class由于上面已经写了包名,这里直接使用即可 -->
<one-to-many class="Emp"/>
</set>
</class>
</hibernate-mapping>
DeptDao.java
中的关键代码如下:
public void saveNewDept(Dept dept){
this.currentSession().save(dept);
}
DeptBiz.java
中的关键代码如下:
public void saveNewDept(Dept dept) {
Transaction transaction = null;
try {
transaction = deptDao.currentSession().beginTransaction();
deptDao.saveNewDept(dept);
transaction.commit();//提交事务
} catch (Exception e) {
e.printStackTrace();
if (transaction != null) {
transaction.rollback();
}
}
}
测试方法关键代码如下:
@Test
public void testSaveDept() throws Exception {
Dept dept = new Dept();
dept.setDeptNo((short)22);
dept.setDeptName("质控部");
dept.setLocation("中区");
Emp emp1 = new Emp();
emp1.setEmpName("李四");
//建立Dept对象和Emp对象的双向关联关系
emp1.setDept(dept);
dept.getEmps().add(emp1);
DeptBiz deptBiz = new DeptBiz();
deptBiz.saveNewDept(dept);
}
测试方法注意:需要使用属性设置的方式建立Dept对象和Emp对象的双向关联关系
<set>
元素中的cascade
属性设置为"save-update"
. Hibernate
在持久化Dept
对象时 , 会自动持久化关联的所有Emp
对象 . Hibernate
执行以下SQL
语句:
Hibernate:
select
max(EMPNO)
from
EMP
Hibernate:
insert
into
DEPT
(DNAME, LOC, DEPTNO)
values
(?, ?, ?)
Hibernate:
insert
into
EMP
(ENAME, JOB, SAL, HiREDATE, DEPTNO, EMPNO)
values
(?, ?, ?, ?, ?, ?)
Hibernate:
update
EMP
set
DEPTNO=?
where
EMPNO=?
语句"update EMP set DEPTNO=? where EMPNO=?"
用来保证级联添加EMP
记录的外键字段能够正确的执行相关的DEPT
记录. 有关此条SQL
语句的产生和优化 , 下文中会有进一步分析 .
下面完成第二个持久化操作 , 删除
Dept
对象 , 并级联删除与Dept
对象关联的Emp
对象. 完成这个操作,如示例11
所示
示例11:
修改Dept.hbm.xml
中<set>
元素的cascade
属性值:
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="cn.hibernatedemo.entity">
<!--<class name="Dept" table="DEPT" dynamic-update="true">-->
<class name="Dept" table="DEPT">
<id name="deptNo" column="DEPTNO">
<generator class="assigned"/>
<!--<generator class="increment"/>-->
</id>
<property name="deptName" column="DNAME"/>
<property name="location" column="LOC"/>
<!-- name指定了映射的集合的属性,即集合实例化的emps;
table指定了集合属性对应的集合表 -->
<!--<set name="emps" table="EMP" cascade="save-update">-->
<set name="emps" table="EMP" cascade="delete">
<!--column指定了集合表的外键 -->
<key column="DEPTNO"></key>
<!-- class由于上面已经写了包名,这里直接使用即可 -->
<one-to-many class="Emp"/>
</set>
</class>
</hibernate-mapping>
DeptDao.java
中的关键代码如下:
//先查询
public Dept load(Serializable id) {
return currentSession().load(Dept.class, id);
}
//删除
public void delete(Dept dept){
this.currentSession().delete(load(dept.getDeptNo()));
}
DeptBiz.java
中的关键代码如下:
public void deleteDept(Dept dept){
Transaction transaction = null;
try {
transaction = deptDao.currentSession().beginTransaction();
deptDao.delete(dept);
transaction.commit();//提交事务
} catch (Exception e) {
e.printStackTrace();
HibernateUtil.rollback(transaction);
}
}
测试方法中的关键代码如下:
@Test
public void testDeleteDeptByDept() throws Exception {
Dept dept = new Dept();
dept.setDeptNo((short) 22);
DeptBiz deptBiz = new DeptBiz();
deptBiz.deleteDept(dept);
}
运行该示例 , Hibernate
会执行以下SQL
语句:
Hibernate:
delete
from
EMP
where
EMPNO=?
Hibernate:
delete
from
DEPT
where
DEPTNO=?
从语句中可以发现 , Hibernate
会删除Dept
对象 , 以及管理的Emp
对象;
注意:进行级联删除时 , 需要先进行查询 , 使得对象的状态变为持久化状态 , 再删除, 这样就能实现级联删除的功能
2. <set>
元素的inverse
属性
术语inverse
直译为反转
. 在Hibernate
中 , inverse
属性指定了关联关系中的方向.
<set>
元素的inverse
属性的值有两个 , 即true
和false
, 默认是false
, 关联关系中,inverse="false"
的为主动方 ,inverse="true"
的为被动方, 主动方会负责维护关联关系 .
注意:若一的一方不负责维护关联关系,则需要使用代码建立多的一方到一的一方之间的关联关系,否则多的一方的外键在级联新增操作时不会更新
例如示例10中的Dept
一方 , 会主动执行执行update
语句 : update emp set DEPTNO=? where empmo=?
, 以维护外键的取值 .
示例12演示了
<set>
元素的inverse
属性不同取值的影响 .
在示例12中先加载持久化类Dept
和Emp
对象 , 然后双向建立两者的关联关系 , 实现调整员工所属的部门 .
示例12:
EmpDao
和 DeptDao
中添加根据OID
加载实例的方法 , 以EmpDao
为例, 关键代码如下:
public Emp load(Serializable empNo){
return this.currentSession().load(Emp.class,empNo);
}
业务层EmpBiz.java
中关键代码如下:
public void changeDept(Integer empNo,Short deptNo) {
Transaction transaction = null;
try {
transaction = empDao.currentSession().beginTransaction();
DeptDao deptDao = new DeptDao();
/*加载Dept 和 Emp 的持久化对象*/
Dept dept = deptDao.load(deptNo);
Emp emp = empDao.load(empNo);
/*建立Dept对象和Emp对象的关联关系*/
emp.setDept(dept);
dept.getEmps().add(emp);
transaction.commit();//提交事务
} catch (Exception e) {
e.printStackTrace();
HibernateUtil.rollback(transaction);
}
}
测试方法关键代码如下:
@Test
public void testChangeDept() throws Exception {
EmpBiz empBiz = new EmpBiz();
empBiz.changeDept(7369,(short)40);
}
**Hibernate
会按照持久化对象的属性变化来同步更新数据库 . **
如果Dept.hbm.xml
文件中<set>
元素的inverse
属性值设置为false
(或者不对inverse
属性进行明确定义,其值默认为false
) , 那么Hibernate
将执行以下两条关键SQL
语句:
Hibernate:
update
EMP
set
ENAME=?,
JOB=?,
SAL=?,
HiREDATE=?,
DEPTNO=?
where
EMPNO=?
Hibernate:
update
EMP
set
DEPTNO=?
where
EMPNO=?
以上SQL
语句表明Hibernate
执行两次update
操作 . Hibernate
根据内存中持久化对象的属性变化来决定需要执行哪些SQL
语句.
示例12中建立
Emp
对象和Dept
对象的双向关联关系时,分别进行了如下修改.
(1) 修改Emp
对象,建立Emp
对象到Dept
对象的多对一关联关系:
emp.setDept(dept);
Hibernate
检查到持久化对象emp
的属性发生变化后,会执行相应的SQL
语句:
update EMP set ENAME=?, JOB=?,SAL=?, HiREDATE=?, DEPTNO=? where EMPNO=?
(2) 修改Dept
对象 , 建立Dept
对象到Emp
的一对多关联关系:
dept.getEmps().add(emp);
因为Dept.hbm.xml
文件中的<set>
元素的inverse
属性值为false
,Dept
方面会主动维护关联关系,所以Hibernate
检查到持久化对象dept
属性的上述变化后 , 会执行如下SQL
语句:
update EMP set DEPTNO=? where EMPNO=?
该语句保证了EMP
表中记录的外键字段能够被正确赋值 .
但是结合示例12的具体代码分析 ,
Emp
对象已经通过emp.setDept(dept)
设定了正确的关联关系 . 所以 , 该语句实际上是多余的 , 执行多余的update
语句会影响应用的性能,这种情况下可以把<set>
元素的inverse
属性值设置为true
:
修改Dept.hbm.xml
文件,关键代码如下:
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="cn.hibernatedemo.entity">
<!--<class name="Dept" table="DEPT" dynamic-update="true">-->
<class name="Dept" table="DEPT">
<id name="deptNo" column="DEPTNO">
<generator class="assigned"/>
<!--<generator class="increment"/>-->
</id>
<property name="deptName" column="DNAME"/>
<property name="location" column="LOC"/>
<!-- name指定了映射的集合的属性,即集合实例化的emps;table指定了集合属性对应的集合表 -->
<!--<set name="emps" table="EMP" cascade="save-update">-->
<!--<set name="emps" table="EMP" cascade="delete" >-->
<!--inverse属性:用来维护外键的取值,
inverse设置为false,则为主动方,由主动方负责维护关联关系,默认是false
inverse设置为true,不负责维护关联关系 -->
<set name="emps" table="EMP" inverse="true" >
<!--column指定了集合表的外键 -->
<key column="DEPTNO"></key>
<!-- class由于上面已经写了包名,这里直接使用即可 -->
<one-to-many class="Emp"/>
</set>
</class>
</hibernate-mapping>
以上配置表明在Dept
和Emp
双向关联关系中 , Dept
端的关联只是Emp
端关联的镜像 . Hibernate
仅按照Emp
对象的关联属性的变化来同步更新数据库 , 而忽略Dept
关联属性的变化.
按照上述方式修改Dept.hbm.xml
, 修改测试条件后再次运行示例12,测试代码如下:
@Test
public void testChangeDept() throws Exception {
EmpBiz empBiz = new EmpBiz();
empBiz.changeDept(7369,(short)30);
}
Hibernate
将执行一条关键的更新语句:
update EMP set ENAME=?,JOB=?, SAL=?, HiREDATE=?,DEPTNO=? where EMPNO=?
接下来对示例12进行如下修改 , 进一步去验证 , 在Dept.hbm.xml
中配置了inverse="true"
后,Hibernate
不会通过Dept
对象的设置 , 而是仅通过Emp
对象来维护两者之间的关联关系 .
(1) 修改示例12中EmpBiz.java
,**不建立Dept
到Emp
对象的关联 **, EmpBiz.java
关键代码如下:
public void changeDept_1(Integer empNo,Short deptNo) {
Transaction transaction = null;
try {
transaction = empDao.currentSession().beginTransaction();
DeptDao deptDao = new DeptDao();
/*加载Dept 和 Emp 的持久化对象*/
//根据部门编号查找部门信息
//根据员工编号查找员工信息
Dept dept = deptDao.load(deptNo);
Emp emp = empDao.load(empNo);
//仅建立Emp对Dept的关联
emp.setDept(dept);
//dept.getEmps().add(emp);
transaction.commit();//提交事务
} catch (Exception e) {
e.printStackTrace();
HibernateUtil.rollback(transaction);
}
}
以上代码仅设置了Emp
对象的dept
属性 , Hibernate
仍能按照Emp
对象的属性变化来同步更新数据库,修改测试代码如下:
@Test
public void testChangeDept_1() throws Exception {
EmpBiz empBiz = new EmpBiz();
empBiz.changeDept(7369,(short)40);
}
Hibernate
执行以下关键SQL
语句:
Hibernate:
update
EMP
set
ENAME=?,
JOB=?,
SAL=?,
HiREDATE=?,
DEPTNO=?
where
EMPNO=?
(2) 修改示例12中EmpBiz.java
,**不建立Emp
到Dept
对象的关联 **, EmpBiz.java
关键代码如下:
public void changeDept_2(Integer empNo,Short deptNo) {
Transaction transaction = null;
try {
transaction = empDao.currentSession().beginTransaction();
DeptDao deptDao = new DeptDao();
/*加载Dept 和 Emp 的持久化对象*/
//根据部门编号查找部门信息
//根据员工编号查找员工信息
Dept dept = deptDao.load(deptNo);
Emp emp = empDao.load(empNo);
//emp.setDept(dept);
//仅建立Dept对Emp的关联关系
dept.getEmps().add(emp);
transaction.commit();//提交事务
} catch (Exception e) {
e.printStackTrace();
HibernateUtil.rollback(transaction);
}
}
修改测试方法代码如下:
@Test
public void testChangeDept_2() throws Exception {
EmpBiz empBiz = new EmpBiz();
empBiz.changeDept(7369,(short)30);
}
运行测试方法,发现Hibernate
并未执行任何update
语句.
**总结:以上代码仅设置了
Dept
对象的emps
属性 , 由于Dept.hbm.xml
文件中<set>
元素的inverse
属性为true
,所以Hibernate
并没执行任何update
语句. **
需要注意的是 , 这也就意味着EMP
表中该员工记录的外键没有得到正确的更新 . 由此可以得到如下操作建议:
- 映射双向一对多的关联关系时 , 在
"一"
方把<set>
元素的inverse
属性设置为true
,可以提高应用的性能. - 在代码中建立两个对象的关联时 , 应该同时修改两个关联对象的相关属性:
- emp.setDept(dept);
- dept.getEmps().add(emp);
这样才能使程序更加健壮 , 提供业务逻辑层的独立性 , 使业务逻辑层的代码不受
Hibernate
实现的影响. 同理,当解除双向关联的关系时 , 也应该同时修改关联两端的对象的相应属性:
- emp.setDept(null);
- dept.getEmps().remove(emp);
4 . 建立多对多关联关系
前面介绍了映射一对多关联关系的方法 . 这是软件开发中最常见的关联关系 . 下面介绍另一种关联关系的映射 : 多对多关联.
一对多关系通常仅涉及两张表 ,
"多"
方通过外键引用"一"
方表的主键来实现"一对多"
的关联 . 而"多对多"
关系除了两张"多"
方的表之外 , 还需要一张额外的表 , 通过外键分别引用两张"多"
方表的主键来实现多对多的关联 .
下面以Project(项目)
类与Employee(员工)
类的关系为例 , 介绍如何映射"多对多"
关联 . 一个项目需要多位员工参与 , 一位员工可能参与多个项目 , 项目和员工之间构成了"多对多"
关系 . PROJECT
表,EMPLOYEE
表如图所示:
注意在关系数据模型中,无法直接表达PROJECT
表和EMPLOYEE
表之间多对多关系 , 需要创建一个连接表PROEMP
, 它同时参照PROJECT
表和EMPLOYEE
表.
PROEMP
表以RPROID
字段 和REMPID
字段为联合主键 . 此外 ,RPROID
字段作为外键参照PROJECT
表, 而REMPID
字段作为外键参照EMPLOYEE
表.
根据业务需要 , 可以配置项目和员工的单向多对多关联 . 也可以配置项目和员工的**双向多对多关联 **. 接下来详细讲解这两种配置 .
4.1 配置单向多对多关联
假定仅建立从Project(项目)
类到Employee(员工)
类的单向多对多关联 . 在Project
类中需要定义集合类型的employees
属性 , 而在Employee
类中不定义和Project
相关的集合类型属性 . 如下图显示了Project
类和Employee
类的关联关系:
定义Employee
类 , 代码如下:
/**
* 雇员类
*/
public class Employee implements Serializable {
private Integer empid;
private String empname;
//省略getter setter
}
定义Employee.hbm.xml
文件,内容如下:
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="cn.hibernatedemo.entity">
<class name="Employee" table="EMPLOYEE">
<id name="empid" column="EMPID">
<!--主键需要手动指定-->
<generator class="assigned"/>
</id>
<property name="empname" column="EMPNAME"/>
</class>
</hibernate-mapping>
定义Project
类 , 并定义employees
属性 , 代码如下:
/**
* 项目类
*/
public class Project implements Serializable {
private Integer proid;
private String proname;
private Set<Employee> employees = new HashSet<>();
//省略getter setter
}
定义Project.hbm.xml
文件 , 代码如下:
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="cn.hibernatedemo.entity">
<class name="Project" table="PROJECT">
<id name="proid" column="PROID">
<generator class="assigned"/>
<!--<generator class="increment"/>-->
</id>
<property name="proname" column="PRONAME"/>
<!-- name指定了映射的集合的属性,即集合实例化的employees;table指定了集合属性对应的集合表 -->
<set name="employees" table="PROEMP" cascade="save-update">
<!--key子元素指定`PROEMP`表的外键`RPROID`,用来参照`PROJECT`表
column指定了集合表的外键
set元素的table属性 对应第三张关联表
key元素的column属性 对应PROJECT表在第三张表的外键
many-to-many 元素 column对应关联对象在第三张表的外键-->
<key column="RPROID"></key>
<!-- class由于上面已经写了包名,这里直接使用即可
column:指定PROEMP表的外键REMPID,用来参照EMPLOYEE表 -->
<many-to-many class="Employee" column="REMPID"/>
</set>
</class>
</hibernate-mapping>
<set>
元素的table
属性指定关系表的名称为PROEMP
.<set>
元素的cascade
属性为"save-update"
,表明保存或更新Project
对象时 , 会级联保存或更新与它关联的Employee
对象.<set>
元素的<key>
子元素指定PROEMP
表的外键RPROID
,用来参照PROJECT
表.<many-to-many>
子元素的class
属性指定employees
集合中存放的是Employee
对象.column
属性指定PROEMP
表的外键REMPID
,用来参照EMPLOYEE
表.
经验:
对于多对多关联 ,
cascade
属性设为"save-update"
是合理的 , 但是不建议把cascade
属性设为"all"
,"delete"
.
如果删除一个Project
对象时 , 级联删除与它关联的所有Employee
对象,由于这些Employee
对象有可能还与其他Project
对象关联 , 因此当Hibernate
执行级联删除时 , 会破坏数据库的 外键参照完整性.
基于以上配置 , 完成以下持久化操作 , 创建两个
Project
对象和两个Employee
对象 , 建立它们之间的关联关系 , 保存Project
对象的同时保存Employee
对象 , 如示例13
所示:
注意:set元素中的cascade属性与inverse属性要分开不同的配置文件设置,在set元素中同时设置这两个属性,否则无法操作关系表。
示例13:
ProjectDao.java
中关键代码如下:
/**
* 项目数据访问层
*/
public class ProjectDao extends BaseDao{
public void save(Project project){
this.currentSession().save(project);
}
}
ProjectBiz.java
代码如下:
/**
* 项目业务层
*/
public class ProjectBiz {
private ProjectDao projectDao = new ProjectDao();
public void addNewProject(Project project){
Transaction transaction = null;
try {
transaction = projectDao.currentSession().beginTransaction();
projectDao.save(project);
transaction.commit();
} catch (Exception e) {
e.printStackTrace();
HibernateUtil.rollback(transaction);
}
}
}
同时修改hibernate.cfg.xml
文件 , 关键代码如下:
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<!--<property name="hibernate.connection.url">jdbc:oracle:thin:@localhost
:1521:orcl</property>-->
<property name="hibernate.connection.url">
jdbc:oracle:thin:@localhost:1521:ORCL
</property>
<property name="hibernate.connection.username">scott</property>
<property name="hibernate.connection.password">tiger</property>
<property name="hibernate.connection.driver_class">oracle.jdbc.driver.OracleDriver</property>
<property name="hibernate.dialect">org.hibernate.dialect.Oracle10gDialect</property>
<property name="hibernate.current_session_context_class">thread</property>
<property name="hibernate.show_sql">true</property>
<property name="hibernate.format_sql">true</property>
<mapping resource="cn/hibernatedemo/entity/Dept.hbm.xml"/>
<mapping resource="cn/hibernatedemo/entity/Emp.hbm.xml"/>
<mapping resource="cn/hibernatedemo/entity/Employee.hbm.xml"/>
<mapping resource="cn/hibernatedemo/entity/Project.hbm.xml"/>
</session-factory>
</hibernate-configuration>
测试方法如下:
@Test
public void testAddNewProject() throws Exception {
Employee employee1 = new Employee();
employee1.setEmpid(1);
employee1.setEmpname("张三");
Employee employee2 = new Employee();
employee2.setEmpid(2);
employee2.setEmpname("李四");
Project project1 = new Project();
project1.setProid(1);
project1.setProname("1号项目");
project1.getEmployees().add(employee1);
project1.getEmployees().add(employee2);
Project project2 = new Project();
project2.setProid(2);
project2.setProname("2号项目");
project2.getEmployees().add(employee1);
ProjectBiz projectBiz = new ProjectBiz();
projectBiz.addNewProject(project1);
projectBiz.addNewProject(project2);
}
当Session
的save()
方法保存project1
对象时 , 向PROJECT
表插入一条记录 , 同时还会分别向EMPLOYEE
和PROEMP
表插入两条记录 , 执行如下insert
语句:
Hibernate:
insert into PROJECT (PRONAME, PROID)
values (?, ?)
Hibernate:
insert into EMPLOYEE (EMPNAME, EMPID)
values (?, ?)
Hibernate:
insert into EMPLOYEE (EMPNAME, EMPID)
values (?, ?)
Hibernate:
insert into PROEMP (RPROID, REMPID)
values (?, ?)
Hibernate:
insert into PROEMP (RPROID, REMPID)
values (?, ?)
当Session
的save()
方法保存project2
对象时 , 向PROJECT
表插入一条记录 , 同时向PROEMP
表插入一条记录 .
由于与project2
对象关联的employee1
对象已经被保存到数据库中,因此不再向EMPLOYEE
表插入记录 . Hibernate
执行如下SQL
语句:
Hibernate:
insert into PROJECT (PRONAME, PROID)
values (?, ?)
Hibernate:
insert into PROEMP (RPROID, REMPID)
values (?, ?)
4.2 配置双向多对多关联
建立从Project
类到Employee
类的双向多对多关联 , 在Project
类中需要定义集合类型employees
,并且在Employee
类中也需要定义集合;类型projects
属性 .
修改Employee.java
, 增加属性projects
,代码如下:
/**
* 雇员类
*/
public class Employee implements Serializable {
private Integer empid;
private String empname;
private Set<Project> projects = new HashSet<>();
//省略getter setter
}
在Employee.hbm.xml
文件中 , 映射Employee
类的projects
属性 , 代码如下:
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="cn.hibernatedemo.entity">
<class name="Employee" table="EMPLOYEE">
<id name="empid" column="EMPID">
<!--主键需要手动指定-->
<generator class="assigned"/>
</id>
<property name="empname" column="EMPNAME"/>
<!--设置外键维护关系inverse属性-->
<set name="projects" table="PROEMP" inverse="true">
<!--key子元素指定`PROEMP`表的外键`REMPID`,用来参照`EMP`表
column指定了集合表的外键
set元素的table属性 对应第三张关联表
key元素的column属性 对应EMP表在第三张表的外键
many-to-many 元素 column对应关联对象在第三张表的外键-->
<key column="REMPID"/>
<!-- class由于上面已经写了包名,这里直接使用即可
column:指定PROEMP表的外键RPROID,用来参照PROJECT表 -->
<many-to-many column="RPROID" class="Project"/>
</set>
</class>
</hibernate-mapping>
对于多对多关联的两端 , 需要把其中一端的<set>
元素的inverse
属性设置为true
. 使用双向多对多关联完成持久化操作 , 同时建立从Project
到Employee
和从Employee
到Project
的关联关系 如示例14所示:
注意:
<many-to-many>
子元素的class
属性指定projects
集合中存放的是Project
对象.column
属性指定PROEMP
表的外键RPROID
,用来参照PROJECT
表.
示例14:
ProjectDao.java
以及业务层ProjectBiz.java
中的关键代码同示例13
.
测试方法中的关键代码如下:
@Test
public void testAddNewProject_1() throws Exception {
//1号员工
Employee employee1 = new Employee();
employee1.setEmpid(1);
employee1.setEmpname("张三");
//2号员工
Employee employee2 = new Employee();
employee2.setEmpid(2);
employee2.setEmpname("李四");
//1号项目
Project project1 = new Project();
project1.setProid(1);
project1.setProname("1号项目");
//2号项目
Project project2 = new Project();
project2.setProid(2);
project2.setProname("2号项目");
//建立项目与雇员之间的关系
//由于是配置了双向的多对多关联
// 所以只需要 使用代码建立单向关联即可
project1.getEmployees().add(employee1);
project1.getEmployees().add(employee2);
project2.getEmployees().add(employee1);
ProjectBiz projectBiz = new ProjectBiz();
projectBiz.addNewProject(project1);
projectBiz.addNewProject(project2);
}
示例14
与 示例13
的运行结果相似 , 不再做详细介绍 .
经验 : 在实际开发过程中 , 如果连接表中除了两个外键 , 还包括其他业务字段 , 根据业务需要 , 可以把多对多关联分解为两个一对多关联
5 . 延迟加载
通过关联关系可以在程序中方便的获取关联对象的数据 , 但是如果从数据库中加载Dept
对象时 , 就同时加载所有关联的Emp
对象 , 而程序实际上仅仅需要访问Dept
对象 , 那么这些关联的Emp
对象就白白浪费了许多内存空间 .
Hibernate
查询Dept
对象时 , 立即查询并加载与之关联的Emp
对象 , 这种查询策略称为立即加载
.立即加载
存在两大不足 :(1)
select
的语句数目太多,需要频繁的访问数据库,会影响查询的性能。(2) 可能会加载大量不需要的对象 , 增加系统开销 , 浪费内存空间.
为了解决以上问题 , Hibernate
提供了延迟加载
策略 .延迟加载:延迟加载(lazy load懒加载)是当在真正需要数据时,才执行SQL语句进行查询。避免了无谓的性能开销。
延迟加载策略能避免加载应用程序不需要访问的关联对象 . 本节以
Dept
类和Emp
类为例 , 介绍如何设置延迟加载 , 以优化查询性能 .
Hibernate
允许对象-关系
映射文件中使用lazy
属性配置加载策略 , 并且可以分为类级和关联级两个级别 , 分别进行控制.下表列出了lazy
属性的常用取值 :
级别 | Lazy 属性取值 |
---|---|
类级别 | <class> 元素中lazy 属性的可选值为true(延迟加载) 和false(立即加载) . 默认值为true |
一对多关联级别 | <set> 元素中lazy 属性的可选值为true(延迟加载) ,extra(增强延迟加载) 和false(立即加载) . 默认值为true |
多对一关联级别 | <many-to-one> 元素中lazy 属性的可选值为proxy(延迟加载) ,no-proxy(增强延迟加载) 和false(立即加载) . 默认值为proxy |
由上表可以看出 , 对于Hibernate
3.x以上的版本 , 无论哪个级别 , 默认采用的都是延迟加载的查询策略 , 以减少系统资源的开销 .
5.1 类级别的查询策略:
类级别可选的加载策略包括立即加载和延迟加载。默认为延迟加载。如果<class>
元素的lazy属性为true。表示采用延迟加载;如果lazy属性为false,表示采用立即加载.
以Emp和Dept为例:
1 . 立即加载
在Dept.hbm.xml中的<class>
元素中添加属性 lazy="false"
表示立即加载
<class name="Dept" table="DEPT" lazy="false">
当测试方法 通过Session
的load()
方法加载Dept
对象时,代码如下:
Session session = HibernateUtil.currentSession();
Transaction transaction = session.beginTransaction();
Dept dept = session.load(Dept.class,(short)10);
transaction.commit();
System.out.println(dept.getDeptName());
Hibernate
会立即执行查询DEPT
表的select
语句:
Hibernate:
select
dept0_.DEPTNO as DEPTNO1_0_0_,
dept0_.DNAME as DNAME2_0_0_,
dept0_.LOC as LOC3_0_0_
from
DEPT dept0_
where
dept0_.DEPTNO=?
2 . 延迟加载
类级别的默认加载策略是延迟加载
. 在Dept.hbm.xml
文件中 , 以下两种方式都表示采用延迟加载策略:
<class name="Dept" table="DEPT">
或者:
<class name="Dept" table="DEPT" lazy="true">
如果程序加载一个持久化对象的目的是访问它的属性 , 则可以采用立即加载 .
如果程序加载一个持久化对象的目的是获得它的引用 , 则可以采用延迟加载.
如
示例15
所示 ,示例15
在Dept
类级别采用延迟加载 , 向数据库保存了一个Emp
对象 , 它与已经存在的一个Dept
持久化对象关联 .
示例15:
Session session = HibernateUtil.currentSession();
Transaction transaction = session.beginTransaction();
Dept dept = session.load(Dept.class, (short) 10);
Emp emp = new Emp();
emp.setEmpName("Tom");
emp.setDept(dept);
session.save(emp);
transaction.commit();
因为Dept
类级别采用延迟加载 , session.load()
方法不会执行访问DEPT
表的select
语句 , 只返回一个Dept
代理类的实例 , 它的deptNo
属性为10 , 其余属性都为null
. 以上代码仅由session.save()
方法执行一条insert
语句,当然为了得到自增主键 , Hibernate
还查询了主键的最大值, 控制台的SQL
语句如下:
Hibernate:
select
max(EMPNO)
from
EMP
Hibernate:
insert
into
EMP
(ENAME, JOB, SAL, HiREDATE, DEPTNO, EMPNO)
values
(?, ?, ?, ?, ?, ?)
当<class>
元素的lazy
属性为true
时 , 会影响Session
的load()
方法的各种运行时行为 , 下面举例说明:
(1) 通过load()
方法加载延迟状态的Dept
代理实例 , 除了OID
, 其他属性均为null
. 通过调用其getDeptName()
等方法可以促使Hibernate
执行查询 , 获得数据从而完成代理实例的初始化.
(2) 调用代理类实例的getDeptNo()
方法访问OID
属性 , 不会触发Hibernate
初始化代理类实例的行为 , 该方法会直接返回Dept
代理类实例的OID
值 , 无须查询数据库 .
(3) org.hibernate.Hibernate
类的initialize()
静态方法用于显示初始化代理类实例 , isInitialized()
方法用于判断代理类实例是否已经被初始化 . 以下代码通过Hibernate
类的initialize()
方法显式初始化了Dept
代理类实例.
Session session = HibernateUtil.currentSession();
Transaction transaction = session.beginTransaction();
Dept dept = session.load(Dept.class, (short) 10);
if(!Hibernate.isInitialized(dept)){
Hibernate.initialize(dept);
}
transaction.commit();
(4)如果加载的Dept
代理实例的OID
在数据库中不存在 , Session
的load()
方法不会立即抛出异常 , 因为此时并未真正执行查询 . 只有当Hibernate
试图完成对Dept
代理实例的初始化时 , 才会真正执行查询语句 , 这时会抛出以下异常:org.hibernate.ObjectNotFoundException : No row with the given identifier exists
(5)Dept
代理类的实例只有在当前Session
范围内才能被初始化 . 如果在当前Session
生命周期内 , 应用程序没有完成Dept
代理实例的初始化工作 , 那么在当前Session
关闭后 , 试图访问该Dept
代理实例中OID
以外的属性(如调用getDeptName()
方法) , 将抛出以下异常:org.hibernate.LazyInitializationException : could not initialize proxy-no Session
值的注意的是 , 无论
Dept.hbm.xml
文件中的<class>
元素的lazy
属性是true
还是false
,Session
的get()
方法以及Query
对象的list()
方法在类级别总是使用立即加载
策略 , 举例说明如下:
- 当通过
Session
的get()
方法加载Dept
对象时:
Session session = HibernateUtil.currentSession();
Transaction transaction = session.beginTransaction();
Dept dept = session.get(Dept.class, (short) 10);
transaction.commit();
Hibernate
会立即执行以下select
语句:
Hibernate:
select
dept0_.DEPTNO as DEPTNO1_0_0_,
dept0_.DNAME as DNAME2_0_0_,
dept0_.LOC as LOC3_0_0_
from
DEPT dept0_
where
dept0_.DEPTNO=?
如果存在相关的数据 , get()
方法就会返回Dept
对象 , 否则就返回null
, get()
方法永远不会返回Dept
的代理类实例 , 这是与load()
方法又一个不同之处
- **当运行
Query
对象的list()
方法时 : **
Session session = HibernateUtil.currentSession();
Transaction transaction = session.beginTransaction();
List<Dept> list = session.createQuery("from Dept", Dept.class).list();
transaction.commit();
Hibernate
立即执行以下select
语句:
Hibernate:
select
dept0_.DEPTNO as DEPTNO1_0_,
dept0_.DNAME as DNAME2_0_,
dept0_.LOC as LOC3_0_
from
DEPT dept0_
5.2 一对多
和多对多
关联的查询策略
在映射文件中 , 用<set>
元素的lazy
属性来配置一对多以及多对多关联关系的加载策略 . Dept.hbm.xml
文件中如下代码配置了Dept
类和Emp
类的一对多关联关系 :
<set name="emps" table="EMP" inverse="true" lazy="true" >
<!--column指定了集合表的外键 -->
<key column="DEPTNO"></key>
<!-- class由于上面已经写了包名,这里直接使用即可 -->
<one-to-many class="Emp"/>
</set>
<set>
元素的lazy
属性的取值决定了emps
集合被初始化的时机 ,即到底在加载Dept
对象时就被初始化 , 还是在程序访问Dept
对象的emps
属性集合时才被初始化 .
<set>
元素的lazy
属性取不同值时设置的查询策略如下表:
lazy属性值 | 加载策略 |
---|---|
true(默认) | 延迟加载 |
false | 立即加载 |
extra | 增强延迟加载 |
1. 立即加载
以下代码表明Dept
类的emps
集合采用立即加载策略 :
**
示例16
: 通过Session
的get()
方法加载OID
为10的Dept
对象 . **
**示例16 : **
修改Dept.hbm.xml
文件 , 内容如下:
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="cn.hibernatedemo.entity">
<!--<class name="Dept" table="DEPT" dynamic-update="true">-->
<class name="Dept" table="DEPT" lazy="false" >
<id name="deptNo" column="DEPTNO">
<generator class="assigned"/>
<!--<generator class="increment"/>-->
</id>
<property name="deptName" column="DNAME"/>
<property name="location" column="LOC"/>
<!-- name指定了映射的集合的属性,即集合实例化的emps;
table指定了集合属性对应的集合表 -->
<!--<set name="emps" table="EMP" cascade="save-update">-->
<!--<set name="emps" table="EMP" cascade="delete" >-->
<!--inverse属性:用来维护外键的取值,
inverse设置为false,则为主动方,由主动方负责维护关联关系,默认是false
inverse设置为true,不负责维护关联关系 -->
<set name="emps" table="EMP" inverse="true" lazy="false" >
<!--column指定了集合表的外键 -->
<key column="DEPTNO"></key>
<!-- class由于上面已经写了包名,这里直接使用即可 -->
<one-to-many class="Emp"/>
</set>
</class>
</hibernate-mapping>
测试方法如下:
Session session = HibernateUtil.currentSession();
Transaction transaction = session.beginTransaction();
Dept dept = session.get(Dept.class, (short) 10);
transaction.commit();
执行Session
的get()
方法时 , 对于Dept
对象的emps
集合(即与Dept
关联的所有Emp
对象) , 若采用一对多
关联级别的的立即加载策略
,Hibernate
会执行以下select
语句:
Hibernate:
select
dept0_.DEPTNO as DEPTNO1_0_0_,
dept0_.DNAME as DNAME2_0_0_,
dept0_.LOC as LOC3_0_0_
from
DEPT dept0_
where
dept0_.DEPTNO=?
Hibernate:
select
emps0_.DEPTNO as DEPTNO6_1_0_,
emps0_.EMPNO as EMPNO1_1_0_,
emps0_.EMPNO as EMPNO1_1_1_,
emps0_.ENAME as ENAME2_1_1_,
emps0_.JOB as JOB3_1_1_,
emps0_.SAL as SAL4_1_1_,
emps0_.HiREDATE as HiREDATE5_1_1_,
emps0_.DEPTNO as DEPTNO6_1_1_
from
EMP emps0_
where
emps0_.DEPTNO=?
通过以上select
语句 , Hibernate
加载了一个Dept
对象和多个Emp
对象 . 但在很多情况下 , 应用程序并不需要访问这些Emp
对象 , 所以在一对多
关联级别中不能随意采用立即加载
策略 .
2. 延迟加载
对于<set>
元素 , 应该优先考虑使用默认的延迟加载
策略:
<set name="emps" table="EMP" inverse="true">
....
</set>
或者:
<set name="emps" table="EMP" inverse="true" lazy="true">
....
</set>
修改Dept.hbm.xml
文件 , 去掉lazy="false"
或者将其修改为lazy="true"
, 内容如下:
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="cn.hibernatedemo.entity">
<!--<class name="Dept" table="DEPT" dynamic-update="true">-->
<class name="Dept" table="DEPT" lazy="false" >
<id name="deptNo" column="DEPTNO">
<generator class="assigned"/>
<!--<generator class="increment"/>-->
</id>
<property name="deptName" column="DNAME"/>
<property name="location" column="LOC"/>
<!-- name指定了映射的集合的属性,即集合实例化的emps;table指定了集合属性对应的集合表 -->
<!--<set name="emps" table="EMP" cascade="save-update">-->
<!--<set name="emps" table="EMP" cascade="delete" >-->
<!--inverse属性:用来维护外键的取值,
inverse设置为false,则为主动方,由主动方负责维护关联关系,默认是false
inverse设置为true,不负责维护关联关系 -->
<set name="emps" table="EMP" inverse="true" >
<!--column指定了集合表的外键 -->
<key column="DEPTNO"></key>
<!-- class由于上面已经写了包名,这里直接使用即可 -->
<one-to-many class="Emp"/>
</set>
</class>
</hibernate-mapping>
运行示例16的测试方法 , 仅加载Dept
对象 , 执行以下select
语句:
Hibernate:
select
dept0_.DEPTNO as DEPTNO1_0_0_,
dept0_.DNAME as DNAME2_0_0_,
dept0_.LOC as LOC3_0_0_
from
DEPT dept0_
where
dept0_.DEPTNO=?
Session
的get()
方法返回的Dept
对象中 , emps
属性引用一个没有没有初始化代理类的实例 . 换句话说 : 此时的emps
集合中没有存放任何Emp
对象 . 只有当emps
集合代理类实例被初始化时 , 才会到数据中查询所有与Dept
关联的所有Emp
对象 , 执行以下SQL
语句:
Hibernate:
select
emps0_.DEPTNO as DEPTNO6_1_0_,
emps0_.EMPNO as EMPNO1_1_0_,
emps0_.EMPNO as EMPNO1_1_1_,
emps0_.ENAME as ENAME2_1_1_,
emps0_.JOB as JOB3_1_1_,
emps0_.SAL as SAL4_1_1_,
emps0_.HiREDATE as HiREDATE5_1_1_,
emps0_.DEPTNO as DEPTNO6_1_1_
from
EMP emps0_
where
emps0_.DEPTNO=?
那么 , Dept
对象的emps
属性引用集合代理类实例何时初始化呢 ? 主要包括以下两种情况:
-
会话关闭前 , 应用程序第一次访问它时 , 如调用它的
iterator(),size(),isEmpty()或者contains()
方法 , 修改测试代码如下:Session session = HibernateUtil.currentSession(); Transaction transaction = session.beginTransaction(); Dept dept = session.get(Dept.class, (short) 10); //查询集合个数,导致emps集合代理类实例被初始化 System.out.println(dept.getEmps().size()); transaction.commit();
-
会话关闭前 , 通过
org.hibernate.Hibernate
类的initialize()
静态方法初始化,修改测试代码如下:Session session = HibernateUtil.currentSession(); Transaction transaction = session.beginTransaction(); Dept dept = session.get(Dept.class, (short) 10); //导致emps集合代理类实例被初始化 Hibernate.initialize(dept.getEmps()); transaction.commit();
3. 增强延迟加载
在<set>
元素中配置lazy
属性为extra
, 表明采用增加延迟加载
策略 :
<set name="emps" inverse="true" lazy="extra">...</set>
增强延迟加载策略
与一般的延迟加载策略
(lazy="true"
)的主要区别在于 :
增强延迟加载策略
能进一步延迟Dept
对象的emps
集合代理类实例的初始化时机 .
当程序第一次访问
emps
属性的size(),contains()和isEmpty()
方法时 ,Hibernate
不会初始化emps
集合代理实例 , 仅通过select
语句查询必要的信息 ;
而当程序访问
emps
属性的iterator()
方法时 ,Hibernate
才会加载emps
集合代理类的实例并初始化 .
以下程序代码演示了采用增强延迟加载策略
时的Hibernate
运行行为 :
演示增强延迟加载
策略 : 在上个示例的基础上修改Dept.hbm.xml
文件,内容如下:
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="cn.hibernatedemo.entity">
<!--<class name="Dept" table="DEPT" dynamic-update="true">-->
<class name="Dept" table="DEPT" lazy="false">
<id name="deptNo" column="DEPTNO">
<generator class="assigned"/>
<!--<generator class="increment"/>-->
</id>
<property name="deptName" column="DNAME"/>
<property name="location" column="LOC"/>
<!-- name指定了映射的集合的属性,即集合实例化的emps;table指定了集合属性对应的集合表 -->
<!--<set name="emps" table="EMP" cascade="save-update">-->
<!--<set name="emps" table="EMP" cascade="delete" >-->
<!--inverse属性:用来维护外键的取值,
inverse设置为false,则为主动方,由主动方负责维护关联关系,默认是false
inverse设置为true,不负责维护关联关系 -->
<set name="emps" table="EMP" inverse="true" lazy="extra" >
<!--column指定了集合表的外键 -->
<key column="DEPTNO"></key>
<!-- class由于上面已经写了包名,这里直接使用即可 -->
<one-to-many class="Emp"/>
</set>
</class>
</hibernate-mapping>
测试方法关键代码如下:
Session session = HibernateUtil.currentSession();
Transaction transaction = session.beginTransaction();
Dept dept = session.get(Dept.class, (short) 10);
//语句不会初始化emps集合代理类实例
//只会执行SQL语句:select count(empno) from emp where deptno=?
System.out.println(dept.getEmps().size());
transaction.commit();
运行测试方法,Hibernate
执行SQL
语句:
Hibernate:
select
dept0_.DEPTNO as DEPTNO1_0_0_,
dept0_.DNAME as DNAME2_0_0_,
dept0_.LOC as LOC3_0_0_
from
DEPT dept0_
where
dept0_.DEPTNO=?
Hibernate:
select
count(EMPNO)
from
EMP
where
DEPTNO =?
通过对SQL
语句的观察 , 并没有发现查询全部emps
的信息 , 只是象征性查询了记录数 .
若要初始化emps
集合代理类的实例 , 并且获取到emps
集合的数据 , 则需要执行以下测试方法,关键代码如下:
Session session = HibernateUtil.currentSession();
Transaction transaction = session.beginTransaction();
Dept dept = session.get(Dept.class, (short) 10);
//若要初始化`emps`集合代理类的实例 , 并且获取到`emps`集合的数据
//则需要执行集合实例的iterator方法
dept.getEmps().iterator();
transaction.commit();
控制台打印出Hibernate
执行的SQL
语句如下:
Hibernate:
select
dept0_.DEPTNO as DEPTNO1_0_0_,
dept0_.DNAME as DNAME2_0_0_,
dept0_.LOC as LOC3_0_0_
from
DEPT dept0_
where
dept0_.DEPTNO=?
Hibernate:
select
emps0_.DEPTNO as DEPTNO6_1_0_,
emps0_.EMPNO as EMPNO1_1_0_,
emps0_.EMPNO as EMPNO1_1_1_,
emps0_.ENAME as ENAME2_1_1_,
emps0_.JOB as JOB3_1_1_,
emps0_.SAL as SAL4_1_1_,
emps0_.HiREDATE as HiREDATE5_1_1_,
emps0_.DEPTNO as DEPTNO6_1_1_
from
EMP emps0_
where
emps0_.DEPTNO=?
通过观察控制台的SQL
语句 , 发现该案例达到了我们对于Dept
对象的emps
属性的增强延迟加载
效果.
5.3多对一
关联的查询策略
在映射文件中 , <many-to-one>
元素用来设置多对一关联关系。lazy
属性: 默认值为proxy
,proxy:延迟加载,no-proxy:无代理延迟加载,false:立即加载。
<many-to-one name="dept" column="DEPTNO" lazy="proxy" class="Dept"/>
<many-to-one>
元素的lazy
属性不同取值时设置的加载策略如下表所示:
lazy属性值 | 加载策略 |
---|---|
proxy(默认) | 延迟加载 |
no-proxy | 无代理延迟加载 |
false | 立即加载 |
**如果没有显式设置lazy
属性 , 那么在多对一
关联级别采用默认的延迟加载策略 . **
假如应用程序仅仅希望访问
Emp
对象 , 并不需要立即访问与Emp
关联的Dept
对象 , 则应该使用默认的延迟加载策略 .
注意:此lazy
属性与<many-to-one>
元素中对应的class
属性所指的类的类级别加载策略有关系 .
1. 延迟加载:
在<many-to-one>
元素中配置lazy
属性为"proxy"
, 延迟加载与Emp
关键的Dept
对象 , 如示例17所示:
示例17:
修改Emp.hbm.xml
文件 , 内容如下:
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="cn.hibernatedemo.entity">
<class name="Emp" table="EMP">
<id name="empNo" column="EMPNO">
<!--<generator class="assigned"/>-->
<generator class="increment"/>
</id>
<property name="empName" column="ENAME"/>
<property name="job" column="JOB"/>
<property name="salary" column="SAL"/>
<property name="hireDate" column="HiREDATE"/>
<!--many-to-one元素建立了EMP表的外键DEPTNO和dept属性之间的映射.
name:设定持久化类的属性名,此处为Emp类的dept属性.
column:设定持久化类的属性对应的表的外键,此处为EMP表的外键DEPTNO
class:设定持久化类的属性的类型,此处设定dept属性为Dept类型-->
<many-to-one name="dept" column="DEPTNO" lazy="proxy" class="Dept"/>
</class>
</hibernate-mapping>
修改测试方法 , 关键代码如下:
Session session = HibernateUtil.currentSession();
Transaction transaction = session.beginTransaction();
Emp emp = session.get(Emp.class,7839);
//emp.getDept()返回的是Dept代理类的实例
Dept dept = emp.getDept();
transaction.commit();
运行测试方法 , 控制台生成的SQL
语句如下:
Hibernate:
select
emp0_.EMPNO as EMPNO1_1_0_,
emp0_.ENAME as ENAME2_1_0_,
emp0_.JOB as JOB3_1_0_,
emp0_.SAL as SAL4_1_0_,
emp0_.HiREDATE as HiREDATE5_1_0_,
emp0_.DEPTNO as DEPTNO6_1_0_
from
EMP emp0_
where
emp0_.EMPNO=?
通过观察SQL
语句发现 , 控制台输出了1条SQL
语句,解释如下:
1条
SQL
语句是查询Emp
对象 , 这是Session
的get()
方法起了作用.而emp.getDept()
返回的是Dept
代理类的实例, 并没有查询数据库 , 符合使用代理对象进行延迟加载
的策略.
2. 无代理延迟加载
将Emp.hbm.xml
文件中<many-to-one>
元素中的lazy
属性修改为no-proxy
, 关键代码如下:
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="cn.hibernatedemo.entity">
<class name="Emp" table="EMP">
<id name="empNo" column="EMPNO">
<!--<generator class="assigned"/>-->
<generator class="increment"/>
</id>
<property name="empName" column="ENAME"/>
<property name="job" column="JOB"/>
<property name="salary" column="SAL"/>
<property name="hireDate" column="HiREDATE"/>
<!--many-to-one元素建立了EMP表的外键DEPTNO和dept属性之间的映射.
name:设定持久化类的属性名,此处为Emp类的dept属性.
column:设定持久化类的属性对应的表的外键,此处为EMP表的外键DEPTNO
class:设定持久化类的属性的类型,此处设定dept属性为Dept类型-->
<many-to-one name="dept" column="DEPTNO" lazy="no-proxy" class="Dept"/>
</class>
</hibernate-mapping>
运行测试方法 ,关键代码如下:
Session session = HibernateUtil.currentSession();
Transaction transaction = session.beginTransaction();
Emp emp = session.get(Emp.class,7839); //第1行 proxy:dept代理对象 no-proxy:dept null
Dept dept = emp.getDept();//第2行
dept.getDeptName();//第3行
transaction.commit();
如果对Emp
对象的dept
属性使用无代理延迟加载
,即<many-to-one>
元素的lazy
属性为no-proxy
, 那么程序第1行加载的dept
为null .
当程序第2行调用
emp.getDept()
方法时 , 将触发Hibernate
执行查询DEPT
表的select
语句 , 从而加载Dept
对象.
由此可见 , 当lazy
属性为proxy
时 , 可以延长延迟加载 Dept
对象的时间 .
而当
lazy
属性为no-proxy
时 , 则可以避免使用由Hibernate
提供的Dept代理类实例
,使Hibernate
对程序提供更加透明的持久化服务.
控制台打印的SQL
语句如下:
Hibernate:
select
emp0_.EMPNO as EMPNO1_1_0_,
emp0_.ENAME as ENAME2_1_0_,
emp0_.JOB as JOB3_1_0_,
emp0_.SAL as SAL4_1_0_,
emp0_.HiREDATE as HiREDATE5_1_0_,
emp0_.DEPTNO as DEPTNO6_1_0_
from
EMP emp0_
where
emp0_.EMPNO=?
Hibernate:
select
dept0_.DEPTNO as DEPTNO1_0_0_,
dept0_.DNAME as DNAME2_0_0_,
dept0_.LOC as LOC3_0_0_
from
SCOTT.DEPT dept0_
where
dept0_.DEPTNO=?
注意:
当lazy
属性为no-proxy
无代理延迟加载时 , 需要在编译期间进行字节码增强操作,否则运行情况和lazy
属性为proxy
时相同 , 因此很少用到。
Java字节码增强指的是在Java字节码生成之后,对其进行修改,增强其功能,这种方式相当于对应用程序的二进制文件进行修改。Java字节码增强主要是为了减少冗余代码,提高性能等。
3. 立即加载
以下代码把Emp.hbm.xml
文件中<many-to-one>
元素的lazy
属性设置为false
:
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="cn.hibernatedemo.entity">
<class name="Emp" table="EMP">
<id name="empNo" column="EMPNO">
<!--<generator class="assigned"/>-->
<generator class="increment"/>
</id>
<property name="empName" column="ENAME"/>
<property name="job" column="JOB"/>
<property name="salary" column="SAL"/>
<property name="hireDate" column="HiREDATE"/>
<!--many-to-one元素建立了EMP表的外键DEPTNO和dept属性之间的映射.
name:设定持久化类的属性名,此处为Emp类的dept属性.
column:设定持久化类的属性对应的表的外键,此处为EMP表的外键DEPTNO
class:设定持久化类的属性的类型,此处设定dept属性为Dept类型-->
<many-to-one name="dept" column="DEPTNO" lazy="false" class="Dept"/>
</class>
</hibernate-mapping>
测试方法代码如下:
Session session = HibernateUtil.currentSession();
Transaction transaction = session.beginTransaction();
Emp emp = session.get(Emp.class, 7839);
//Dept dept = emp.getDept();
transaction.commit();
在运行session.get()
方法时 , Hibernate
执行以下select
语句:
Hibernate:
select
emp0_.EMPNO as EMPNO1_1_0_,
emp0_.ENAME as ENAME2_1_0_,
emp0_.JOB as JOB3_1_0_,
emp0_.SAL as SAL4_1_0_,
emp0_.HiREDATE as HiREDATE5_1_0_,
emp0_.DEPTNO as DEPTNO6_1_0_
from
EMP emp0_
where
emp0_.EMPNO=?
Hibernate:
select
dept0_.DEPTNO as DEPTNO1_0_0_,
dept0_.DNAME as DNAME2_0_0_,
dept0_.LOC as LOC3_0_0_
from
DEPT dept0_
where
dept0_.DEPTNO=?
Hibernate:
select
emps0_.DEPTNO as DEPTNO6_1_0_,
emps0_.EMPNO as EMPNO1_1_0_,
emps0_.EMPNO as EMPNO1_1_1_,
emps0_.ENAME as ENAME2_1_1_,
emps0_.JOB as JOB3_1_1_,
emps0_.SAL as SAL4_1_1_,
emps0_.HiREDATE as HiREDATE5_1_1_,
emps0_.DEPTNO as DEPTNO6_1_1_
from
EMP emps0_
where
emps0_.DEPTNO=?
5.4 Open Sesion In View
模式
什么是Open Session In View
模式
hibernate
中使用load
方法时,并未把数据真正获取时就关闭了session
,当我们真正想获取数据时会迫使load
加载数据,而此时session
已关闭,所以就会出现异常。
比较典型的是在MVC
模式中,我们在M层
调用持久层获取数据时(持久层用的是load
方法加载数据),当这一调用结束时,session
随之关闭,而我们 希望在V层
使用这些数据,这时才会迫使load
加载数据,我们就希望这时的session
是open
着的,这就是所谓的Open Session In view
。
其实比较好的理解就是,关于
Session
关闭的问题,如果我们在数据访问层
关闭了Session
,那么我们在业务逻辑层
想要获取,延迟加载的数据时,就会抛出异常。
相对应的如果业务逻辑层
关闭Session
,那么也会在表示层
获取数据的时候出现异常。
所以我们的解决方法就是当访问网页的时候,请求开始和响应结束的时候,使用同一个
Session
。
而这个模式的主要思想是 : 在用户每次请求过程中 , 始终保持一个Session
对象处于开启状态。
Open Session In View
模式的具体实现有以下三个步骤:
第一步 :
把Session
绑定到当前线程上 , 要保证在一次请求中只有一个Session
对象 . Dao
层HibernateUtil.currentSession()
方法使用SessionFactory
的getCurrentSession()
方法获得Session
, 可以保证每次请求的处理线程上只有一个Session
对象存在.
第二步 :
用Filter
过滤器在请求到达时打开Session
, 在页面生成完毕时关闭Session
, 如示例18所示 .
示例18:
需求说明:在
WEB
页面根据部门编号查询部门以及雇员信息.
在java
目录下新建类 , 类的完全限定名为:cn.hibernatedemo.filter.OpenSessionInViewFilter
, 实现javax.servlet.Filter
接口 , 重写接口中的方法 , 代码如下:
/**
* 开启Open Session In View模式
*/
public class OpenSessionInViewFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
Transaction transaction = null;
try {
//请求到达时,打开Session并开启事务
transaction = HibernateUtil.currentSession().beginTransaction();
//执行请求处理
chain.doFilter(request,response);
transaction.commit();//提交事务之后,自动关闭session
}catch (HibernateException e){
e.printStackTrace();
HibernateUtil.rollback(transaction);//回滚事务
}
}
@Override
public void destroy() {
}
}
修改pom.xml
加入依赖 , 将项目的打包方式修改为war
, 同时加入maven-tomcat
插件,内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>ssh-ch04</artifactId>
<packaging>war</packaging>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<hibernate-core.version>5.4.2.Final</hibernate-core.version>
<slf4j-simple.version>1.7.5</slf4j-simple.version>
<ojdbc6.version>11.2.0.1.0</ojdbc6.version>
<junit.version>4.12</junit.version>
<javax.servlet-api.version>3.1.0</javax.servlet-api.version>
<javax.servlet.jsp-api.version>2.2.1</javax.servlet.jsp-api.version>
<jstl.version>1.2</jstl.version>
</properties>
<dependencies>
<!--hibernate-core -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>${hibernate-core.version}</version>
</dependency>
<!--slf4j-log4j -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>${slf4j-simple.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${slf4j-simple.version}</version>
</dependency>
<!-- junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
</dependency>
<!--ojdbc -->
<dependency>
<groupId>com.oracle</groupId>
<artifactId>ojdbc6</artifactId>
<version>${ojdbc6.version}</version>
</dependency>
<!-- servlet -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>${javax.servlet-api.version}</version>
<scope>provided</scope>
</dependency>
<!-- jsp -->
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>javax.servlet.jsp-api</artifactId>
<version>${javax.servlet.jsp-api.version}</version>
<scope>provided</scope>
</dependency>
<!-- jstl -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>${jstl.version}</version>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<!--扫描到resources下的xml等资源文件-->
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<!-- tomcat7-maven-plugin -->
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<port>8080</port>
<path>/</path>
</configuration>
</plugin>
</plugins>
</build>
</project>
在该模块java
目录下 , 创建类:cn.hibernatedemo.web.DeptServlet
, 代码如下:
/**
* 部门信息表现层
*/
public class DeptServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("utf-8");
String deptno = req.getParameter("deptno");
DeptBiz deptBiz = new DeptBizImpl();
Dept dept = deptBiz.findDeptById(Short.parseShort(deptno));
req.setAttribute("dept",dept);
req.getRequestDispatcher("index.jsp").forward(req,resp);
}
}
在该子模块的main
目录下 新建 webapp
目录 , 并在webapp
目录下新建WEB-INF
目录 , 继续在WEB-INF
目录下新建web.xml
文件 , 配置此Servlet
, 配置过滤器
, 代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0"
metadata-complete="true">
<servlet>
<servlet-name>dept</servlet-name>
<servlet-class>
cn.hibernatedemo.web.DeptServlet
</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>dept</servlet-name>
<url-pattern>/dept</url-pattern>
</servlet-mapping>
<filter>
<filter-name>openSessionInView</filter-name>
<filter-class>
cn.hibernatedemo.filter.OpenSessionInViewFilter
</filter-class>
</filter>
<filter-mapping>
<filter-name>openSessionInView</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
说明:每次请求都在OpenSessionInViewFilter
过滤器中打开Session
, 开启事务 : 页面生成完毕之后 , 在OpenSessionInViewFilter
过滤器中结束事务 并 关闭Session
.
第三步 :
调整业务层代码 , 删除和会话以及事务管理相关的代码 , 仅保留业务逻辑代码 , 如
示例19
所示:
示例19:
DeptBizImpl.java
关键代码如下:
/**
* 部门业务层
*/
public class DeptBizImpl implements DeptBiz {
private DeptDao deptDao = new DeptDaoImpl();
@Override
public Dept findDeptById(Serializable id) {
// Transaction transaction =
// null;
Dept dept = null;
// try {
// transaction =
// HibernateUtil.currentSession().beginTransaction();
dept = deptDao.loadDept(id);
// transaction.commit();
// } catch (Exception e) {
// e.printStackTrace();
// HibernateUtil.rollback(transaction);
// }
return dept;
}
}
数据访问层风格不变 .
表示层在webapp
下 新建 index.jsp
,内容如下:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>部门信息</title>
</head>
<body>
<h2>部门名称:${dept.deptName}</h2>
<h2>该部门下的雇员有:</h2>
<c:forEach items="${dept.emps}" var="emp">
${emp.empName}<br>
</c:forEach>
</body>
</html>
使用maven的方式运行项目 , WEB
浏览器上输入地址http://localhost:8080/dept?deptno=10
, 页面正常输出数据 .
总结 :
**在OpenSessionInViewFilter
过滤器中获取Session
对象 , 保证了一次请求过程中始终使用一个Session
对象 . **
视图
JSP
从一个对象导航到与之关联的对象或集合 , 即使这些对象或集合是被延迟加载的 , 因为当前Session
没有关闭 , 所以能顺利地获取到关联对象或者集合的数据 . 直到视图JSP
数据全部响应完成 ,OpenSessionInViewFilter
过滤器才结束事务并关闭Session
6. Maven插件补充之Jetty:
6.1 什么是Jetty:
Jetty官网地址:https://www.eclipse.org/jetty/
Jetty是当下非常流行的一款轻量级Java Web服务器和Servlet容器实现,它由Eclipse基金会托管,完全免费而且开放源代码,因此所有人均可以从其官网下载最新源代码进行研究。
由于其轻量、灵活的特性,Jetty被广泛用于一系列知名产品,例如ActiveMQ、Maven、Spark、Google App Engine、Eclipse、Hadoop等等。
备注:
服务器软件:运行在服务器操作系统之上,绑定了服务器特定的IP地址,并且在某一个端口监听用户的请求,提供服务的软件。
6.2 为什么使用Jetty?
- 异步的 Servlet,支持更高的并发量
- 模块化的设计,更灵活,更容易定制,也意味着更高的资源利用率
- 在面对大量长连接的业务场景下,Jetty 默认采用的 NIO 模型是更好的选择
- 将jetty嵌入到应用中,使一个普通应用可以快速支持 http 服务
6.3 安装Jetty
- 首先从 jetty 官方网站下载最新的 jetty的zip包,官网地址:
https://www.eclipse.org/jetty/download.html
- 将下载的压缩包解压到指定目录,比如:
D:\Java
下
6.4 Jetty目录结构
Jetty核心目录介绍:
目录 | 作用 |
---|---|
bin/ | 用于存放jetty的启动脚本,目前仅提供liunx/unix下的脚本 |
demo-base/ | 存放一个用于演示的工程 |
etc/ | jetty存放xml配置文件的目录 |
lib/ | 存放jetty运行时依赖的jar包,包括jetty各个模块的jar包,可以说这里就是真正的jetty |
logs/ | 存放请求日志的目录 |
modules/ | 模块定义目录,存放jetty模块定义文件(*.mod) |
resources/ | 存放类路径下的资源文件,比如log4j.xml,存放在这个目录下的文件在jetty启动时会被加入到classpath |
start.ini | 存放命令行启动jetty时需要的一些列参数(可以修改jetty端口) |
start.jar | 用于启动jetty的jar文件 |
webapps | 运行于Jetty默认配置下的Web应用部署目录(一般实际部署应用会重新创建一个另外一个目录作为Jetty_base路径) |
- etc : 存放的都是jetty的配置文件;
- modules : 是存放着各个模块的,以.mod结尾,点进去可以看到有众多模块,不过大多数是没有激活的,像logs,webapps这种模块就是默认激活的。
- webapps : 和Tomcat的webapps一样,用于存放项目的;
- start.jar : 启动Jetty引导java程序,可以在各个操作系统中使用它启动jetty服务,
可以看到截图中是有个work目录的,正常情况下,解压jetty是没有这个目录的
因为当在webapps中存放项目时,通过在根目录下面运行java -jar start.jar命令,启动jetty,由于jetty本身所在的目录和运行的项目的路径是分开的,目的是方便jetty升级的时候,并不影响运行的项目,所以如果在webapps下放置一个war包,运行jetty,该war包解压出来的项目是存在于系统的Temp(
C:\Users\Administrator\AppData\Local\Temp
)目录下面的,在jetty的目录中并不能找到,不过当创建一个work文件夹的时候,解压出来的项目就会默认的存放在work文件夹内了。
6.5 启动
Jetty 的启动跟 Tomcat 不同,一般情况下都可以通过 start.jar 包来启动 jetty,除此之外,在 linux/unix 下还可以通过 jetty.sh 来进行启动。
-
运行 cmd 命令进入 dos 环境,并进入 jetty 安装目录下:
java -jar start.jar
打开浏览器访问 http://localhost:8080/
2. 运行 jetty 提供的示例工程,命令如下:
cd demo-base
java -jar ../start.jar
打开浏览器访问 http://localhost:8080/
备注:
jetty服务的默认端口号是:8080,可以在start.ini 修改端口,例如:jetty.http.port=8081,如图所示:
6.6 部署项目
- 将自己的项目复制到 jetty 目录的 webapps 目录下。
- 在jetty目录下创建work目录,如图所示:
- 开始部署项目,运行 java -jar start.jar
- 打开浏览器访问
http://localhost:8080/ssh_ch05/dept?deptNo=30
6.7 Maven项目中使用Jetty
在pom.xml文件中<build>
元素中加入以下代码:
<plugins>
<plugin>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>9.4.24.v20191120</version>
<configuration>
<httpConnector>
<!--设置jetty的端口号-->
<port>8088</port>
<!--<host>localhost</host>-->
</httpConnector>
<scanIntervalSeconds>5</scanIntervalSeconds>
</configuration>
</plugin>
</plugins>
参数说明如下:
- httpConnector:可选的。如果没有指定,Jetty将创建监听端口8080的ServerConnector实例。
- port : jetty服务器的端口号。
- host : jetty服务器监听的地址。
- scanIntervalSeconds : 扫描进行热部署的间隔时间。
6.8 Jetty与Tomcat的对比:
- Jetty 比较容易贴合第三方框架,比如你可以直接用 Spring 配置一个 Jetty 服务器
- 直接可以将 Jetty 作为提供 HTTP 服务的组件,嵌入到应用中
- Jetty 是面向 Handler 的架构,而 Tomcat 是面向容器的架构
- Jetty 默认采用 NIO 技术,而 Tomcat 默认是 BIO
- Jetty 高度模块化,可以很灵活的管理拓展组件,而 Tomcat 对其他组件的管理则相对困难
6.9 说明:
有关更详细的Jetty文档请参考官网:https://www.eclipse.org/jetty/