一、了解关联关系
-
类与类之间最普遍的关系就是关联关系,并且关联是有方向的。以部门(Dept)和员工(Emp)类为例,一个部门下有多个员工,而一个员工只能属于一个部门。从Emp----->Dept是多对一关联,这就意味着每个Emp对象只会引用一个Dept对象;而从Dept—>Emp是一对多关联,这就意味着每个Dept对象会引用一组Emp对象。因此,在Emp类中应该定义一个Dept类型的属性,来引用所关联的Dept对象;而在Dept类中应该定义一个集合类型的属性,来引用所有关联的Emp对象。
-
如果仅有从Emp到Dept的关联,或者仅有从Dept到Emp的关联,就称为单向关联;如果同时包含两种关联,就称为双向关联。
二、建立单向多对一关联关系
关键步骤:
-
编写Dept和Emp持久化类并配置映射文件。
-
使用
<many-to-one>
元素建立Emp的外键DEPTNO和dept属性之间的映射。 -
验证单向多对一关联关系对象持久化方法。
(1)编写Dept和Emp持久化类并配置映射文件
Dept.java
/**
* 部门表
*/
public class Dept implements Serializable {
/**
* 部门编号
*/
private Integer deptNo;
/**
* 部门名称
*/
private String dName;
/**
* 部门地区
*/
private String loc;
//省略多个getter/setter方法
}
Dept.hbm.xml
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="cn.demo.po.Dept" table="DEPT" schema="scott" dynamic-update="true">
<id name="deptNo" type="java.lang.Integer" column="DEPTNO">
<generator class="assigned"/>
</id>
<property name="dName" type="java.lang.String" column="DNAME"/>
<property name="loc" type="java.lang.String">
<column name="LOC"></column>
</property>
</class>
</hibernate-mapping>
Emp.java:建立单向多对一的关系,故要在该实体类中添加Dept对象属性
/**
* 员工类
*/
public class Emp {
/**
* 员工编号
*/
private Integer empNo;
/**
* 员工姓名
*/
private String ename;
/**
* 员工职位
*/
private String job;
/**
* 上级编号
*/
private Integer mgr;
/**
* 入职日期
*/
private Date hiredate;
/**
* 工资
*/
private Double sal;
/**
* 福利
*/
private Double comm;
/**
* 部门编号
*/
private Integer deptNo;
/**
* 员工所属部门
*/
private Dept dept;
//省略多个getter/setter方法
}
(2)使用<many-to-one>
元素建立Emp的外键DEPTNO和dept属性之间的映射
Emp.hbm.xml
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="cn.demo.po.Emp" table="EMP" schema="scott" dynamic-update="true">
<id name="empNo" type="java.lang.Integer" column="EMPNO">
<generator class="increment"/>
</id>
<property name="ename" type="java.lang.String" column="ENAME"/>
<!--<property name="deptNo" type="java.lang.Integer" column="DEPTNO"/>-->
<property name="mgr" type="java.lang.Integer" column="MGR"/>
<property name="job" type="java.lang.String" column="JOB"/>
<property name="sal" type="java.lang.Double" column="SAL" />
<property name="comm" type="java.lang.Double" column="COMM" />
<property name="hiredate" type="java.util.Date" column="HIREDATE"/>
<!--建立Emp与Dept的多对一关联关系-->
<many-to-one name="dept" column="DEPTNO" class="cn.demo.po.Dept"/>
</class>
</hibernate-mapping>
说明:
【1】<many-to-one>
元素建立了Emp表的外键DEPTNO和dept属性之间的映射。
属性如下:
-
name:设定持久化类的属性名,此处为Emp类的dept属性。
-
column:设定持久化类的属性对应表的外键,此处为Emp表的外键DEPTNO。
-
class:设定持久化类的属性的类型,此处设定dept属性为Dept类型。
-
此处做多对一映射无需再使用以下语句,不然会报异常:重复映射列
<property name="deptNo" type="java.lang.Integer" column="DEPTNO"/>
(3)验证单向多对一关联关系对象持久化方法
【1】添加或修改Emp对象,外键信息封装在Dept对象中,需建立Emp对象和Dept对象的关联,最后保存或更新Emp对象。
EmpDao.java
/**
* 添加Emp
* @param emp
*/
public void save(Emp emp){
this.currentSession().save(emp);
}
EmpBiz.java
/**
* 添加Emp
* @param emp
*/
public void addNewEmp(Emp emp){
Transaction tx=null;
try {
tx=empDao.currentSession().beginTransaction(); //开启事务
empDao.save(emp);
tx.commit();
}catch (HibernateException e){
e.printStackTrace();
if(tx!=null){
tx.rollback();
}
}
}
测试类
/**
* 测试添加Emp员工信息方法
*/
@Test
public void testAddNewEmp(){
//创建Emp对象
Emp emp=new Emp();
emp.setEname("李四");
//指定员工所在的部门为会计部门
Dept dept=new Dept();
dept.setDeptNo(10);
emp.setDept(dept);
//保存员工数据
new EmpBiz().addNewEmp(emp);
}
【2】按照指定的Dept对象来查询相关的Emp对象
EmpDao.java
/**
* 根据部门编号获取员工列表
* @param dept
* @return
*/
public List<Emp> findByDept(Dept dept){
String hql="from Emp where dept=?";
return currentSession().createQuery(hql).setParameter(0,dept).list();
}
EmpBiz.java
/**
* 根据部门编号获取Emp列表
* @param dept
* @return
*/
public List<Emp> findEmpsByDept(Dept dept){
Transaction tx=null;
List<Emp> result=null;
try {
tx=empDao.currentSession().beginTransaction();
result=empDao.findByDept(dept);
tx.commit();
}catch (HibernateException e){
e.printStackTrace();
if(tx!=null){
tx.rollback();
}
}
return result;
}
测试类
/**
* 测试根据部门编号获取Emp列表
*/
@Test
public void testFindEmpsByDept(){
Dept dept=new Dept();
dept.setDeptNo(10);
List<Emp> empList=new EmpBiz().findEmpsByDept(dept);
for(Emp emp:empList){
System.out.println("员工姓名:"+emp.getEname()+",部门名称:"+emp.getDept().getdName());
}
}
【3】输出指定Emp集合中所有的Emp对象及其关联的Dept对象的信息
EmpDao.java
/**
* 查询所有员工:使用Query对象的list()方法
* @return
*/
public List<Emp> findAll(){
//构建Query对象
Query query=currentSession().createQuery("from Emp");
return query.list();
}
EmpBiz.java
/**
* 获取所有员工列表
* @return
*/
public List<Emp> findAllEmpList(){
Transaction tx=null;
List<Emp> result=null;
try {
tx=empDao.currentSession().beginTransaction();
result=empDao.findAll();
for(Emp emp:result){
System.out.print("员工姓名:"+emp.getEname()+"\t");
System.out.println("所在部门 :"+emp.getDept().getdName()+"\t");
}
tx.commit();
}catch (HibernateException e){
e.printStackTrace();
if(tx!=null){
tx.rollback();
}
}
return result;
}
测试类
/**
* 测试获取所有员工列表及相应的部门信息
*/
@Test
public void testFindAllEmpList(){
List<Emp> empList=new EmpBiz().findAllEmpList();
}
三、建立双向一对多关联关系
关键步骤如下:
- 在Dept类中增加一个集合类型的emps属性
- 使用
<set>
元素映射emps属性 - 验证双向一对多关联关系对象持久化方法
(1)在Dept类中增加一个集合类型的emps属性
public class Dept{
private Set<Emp> emps=new HashSet<Emp>(); //部门所包含的员工
public Set<Emp> getEmps() {
return emps;
}
public void setEmps(Set<Emp> emps) {
this.emps = emps;
}
//省略其他属性
}
(2)使用<set>
元素映射emps属性
Dept.hbm.xml
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="cn.demo.po.Dept" table="DEPT" schema="scott" dynamic-update="true">
<id name="deptNo" type="java.lang.Integer" column="DEPTNO">
<generator class="increment"/>
</id>
<property name="dName" type="java.lang.String" column="DNAME"/>
<property name="loc" type="java.lang.String">
<column name="LOC"></column>
</property>
<!--映射Dept与Emp双向一对多关联关系-->
<set name="emps">
<key column="DEPTNO"/>
<one-to-many class="cn.demo.po.Emp"/>
</set>
</class>
</hibernate-mapping>
说明:
【1】<set>
元素的name属性:设定持久化类的属性名,此处为Dept类的emps属性。
【2】<set>
元素还包含另外两个子元素:
<key>
子元素:column属性设定与所关联的持久化类相对应的表的外键,此处为Emp表中的DEPTNO字段。<one-to-many>
子元素:表示emps集合中存放的是一组Emp对象。
(3)验证双向一对多关联关系对象持久化方法
基于关联关系除了可以通过对象间导航实现相关对象的自动检索外,还可以在
对象的增删改操作中,对相关对象实现自动化的级联处理,而无需进行相关编码,从而减少编码工作量,提高开发效率。级联操作的细节可以在持久化类的映射文件通过cascade属性和inverse属性进行控制。
(1)cascade属性
- Dept类、Emp类及其映射文件编写完成后,从Dept到Emp的双向一对多的关联已经建立。
- 在对象-关系映射文件中,用于映射持久化类之间关联关系的元素,如
<set>
、<many-to-one>
都有一个cascade属性,它用于指定如何操纵与当前对象关联的其他对象。
cascade属性的部分常用可选值如下:
-
(1)none(cascade属性默认值):当Session操纵当前对象时,忽略其他关联的对象。
-
(2)save-update:当通过Session的save()、update()及saveOrUpdate()方法来保存或更新
当前对象时,级联保存所有关联的瞬时状态的对象,并且级联更新所有关联的游离状态的对象。 -
(3)delete:当通过Session的delete()方法删除当前对象时,会级联删除所有关联的对象。
-
(4)all:包含save-update、delete的行为。
【例1:使用cascade属性进行级联添加操作】
将Dept.hbm.xml中设置元素的cascade属性为save-update,表示Hibernate在
持久化Dept对象时,会自动持久化关联的所有Emp对象,语句如下:
<set name="emps" cascade="save-update">...</set>
DeptDao.java
/**
* 新增部门信息
* @param dept
*/
public void save(Dept dept){
this.currentSession().save(dept); //保存指定的Dept对象
}
DeptBiz.java
/**
* 新增部门
* @param dept
*/
public void addNewDept(Dept dept){
Transaction tx=null;
try {
tx=deptDao.currentSession().beginTransaction(); //开启事务
deptDao.save(dept);
tx.commit();
}catch (HibernateException e){
e.printStackTrace();
if(tx!=null){
tx.rollback();
}
}
}
测试类
/**
* 测试使用cascade属性级联添加Dept操作
*/
@Test
public void testAddNewDept(){
//创建一个Dept对象和一个Emp对象
Dept dept=new Dept(22,"质控部","中部");
Emp emp=new Emp();
emp.setEname("王五");
//建立Dept对象与Emp对象的双向关联关系
emp.setDept(dept);
dept.getEmps().add(emp);
//保存Dept对象
new DeptBiz().addNewDept(dept);
}
运行结果:
cascade属性的值设为“save-update”,Hibernate在持久化Dept对象时,会自动持久化关联的所有Emp对象。Hibernate会执行以下sql语句:
语句 “update emp set DEPTNO=? WHERE empno=?” 用来保证级联添加的EMP记录的外键字段能够正确指向相关的DEPT记录。
【例2:使用cascade属性级联删除操作】
修改Dept.hbm.xml元素cascade属性为delete,语句如下
<set name="emps" cascade="delete">...</set>
DeptDao.java
/**
* 根据id获取Dept对象:使用session.load()方式
* @param id
* @return Dept对象
*/
public Dept load(Serializable id){
//通过Session的load()方法根据OID加载指定对象
return (Dept)currentSession().load(Dept.class,id);
}
/**
* 设置cascade属性删除部门
* @param dept
*/
public void deleteDept(Dept dept){
this.currentSession().delete(this.load(dept.getDeptNo()));
}
DeptBiz.java
/**
* 设置casecade属性delete,级联删除Dept
* @param dept
*/
public void deleteDept(Dept dept){
Transaction tx=null;
try {
tx=deptDao.currentSession().beginTransaction();
deptDao.deleteDept(dept);
tx.commit();
}catch (HibernateException e){
e.printStackTrace();
if(tx!=null){
tx.rollback();
}
}
}
测试类
/**
* 测试级联删除Dept对象的同时删除Emp对象
*/
@Test
public void testDeleteDept(){
//封装待删除的Dept对象
Dept dept=new Dept();
dept.setDeptNo(41);
//删除该Dept对象
new DeptBiz().deleteDept(dept);
}
运行结果:删除Dept对象的之前,将与其关联的Emp对象全部删除
(2)<set>
元素的inverse属性
-
“inverse"直译为"反转”,在Hibernate中,inverse属性指定了关联关系中的方向。
-
<set>
元素的inverse属性的取值有两个:即true和false,默认false。 -
关联关系中,inverse="false"的一方为主动方,主动方会负责维护关联关系。
【例1:修改指定员工的部门信息】
分别为EmpDao.java和DeptDao.java创建load()方法
/**
* 获取指定Emp对象
* @param empNo 员工编号
* @return
*/
public Emp load(Serializable empNo){
return (Emp) currentSession().load(Emp.class,empNo);
}
/**
* 获取Dept对象
* @param 部门编号
* @return
*/
public Dept load(Serializable id){
//通过Session的get()方法根据OID加载指定对象
return (Dept)currentSession().load(Dept.class,id);
}
EmpBiz.java
/**
* 修改员工所属部门
* @param empNo 员工编号
* @param deptNo 部门编号
*/
public void changeDept(Integer empNo,Integer deptNo){
Transaction tx=null;
try {
tx=empDao.currentSession().beginTransaction();
//加载Emp和Dept的持久化对象
Dept dept=new DeptDao().load(deptNo);
Emp emp=empDao.load(empNo);
//建立Dept对象和Emp对象的关联关系
emp.setDept(dept); //Hibernate检查到持久化对象emp的属性发生改变后,在进行commit时会执行一次update语句
dept.getEmps().add(emp); //持久化对象dept上的属性emps发生改变,会再次执行一次update语句,故共执行两次
//提交事务
tx.commit();
}catch (HibernateException e) {
e.printStackTrace();
if (tx != null) {
tx.rollback();
}
}
}
测试类
/**
* 测试更改员工部门信息
*/
@Test
public void testChangeDept(){
new EmpBiz().changeDept(7369,41);
}
运行结果
从运行结果可以看出:修改成功,但是update语句执行了两次。
说明:
Hibernate会按照持久化对象的属性变化来同步更新数据库。如果Dept.hbm.xml文件中的<set>
元素的inverse属性值设置为false(或不设置,其默认值false),那么Hibernate将重复执行 两次 以下关键SQL语句。
update scott.EMP set DEPTNO=? where EMPNO=?
以上SQL语句表明Hibernate执行了两次update操作。Hibernate根据内存中持久化对象的属性变化来决定需要执行哪些SQL语句。在以上demo中,建立Emp对象和Dept对象的双向关联关系时,分别进行了如下修改:
- (1)修改Emp对象,建立Emp对象到Dept对象的多对一关联关系。
emp.setDept(dept);
Hibernate检查到持久化对象emp的属性发生变化后,会执行相应的sql语句。
- (2)修改Dept对象,建立Dept对象到Emp对象的一对多关联关系。
dept.getEmps.add(emp);
因为Dept.hbm.xml文件中元素的inverse属性值为false,Dept一方会主动维护关联关系,所以Hibernate检查到持久化对象dept属性的上述变化后,会执行相应的修改sql语句:update scott.EMP set DEPTNO=? where EMPNO=?
但是从代码中分析,Emp对象已经通过emp.setDept(dept)设定了正确的关联关系,故dept.getEmps.add(emp)语句实际上是多余的,执行多余的update语句会影响应用的性能。因此在这种情况下可以把<set>
元素的inverse属性值设置为true。
<set name="emps" cascade="all" inverse="true"></set>
以上配置表明在Dept和Emp的双向关联关系中,Dept端的关联只是Emp端关联的镜像。Hibernate仅按照Emp对象的关联属性的变化来同步更新数据库,而忽略Dept关联属性的变化。再次执行程序,Hibernate将仅执行一条关键更新语句。
对代码再次进行修改,进一步验证Dept.hbm.xml中配置了inverse=“true后,Hibernate不会通过Dept对象的设置,而仅通过Emp对象来维护二者间的关联关系。
- (1)修改EmpBiz.java,不建立Dept到Emp对象的关联
public void changeDept(Integer empNo,Integer deptNo){
Transaction tx=null;
try {
tx=empDao.currentSession().beginTransaction();
//加载Emp和Dept的持久化对象
Dept dept=new DeptDao().load(deptNo);
Emp emp=empDao.load(empNo);
//仅建立Emp对象和Dept对象的关联关系
emp.setDept(dept);
//提交事务
tx.commit();
}catch (HibernateException e) {
e.printStackTrace();
if (tx != null) {
tx.rollback();
}
}
}
- (2)修改EmpBiz.java,不建立Emp到Dept对象的关联
public void changeDept(Integer empNo,Integer deptNo){
Transaction tx=null;
try {
tx=empDao.currentSession().beginTransaction();
//加载Emp和Dept的持久化对象
Dept dept=new DeptDao().load(deptNo);
Emp emp=empDao.load(empNo);
//建立Dept对象和Emp对象的关联关系
dept.getEmps().add(emp);
//提交事务
tx.commit();
}catch (HibernateException e) {
e.printStackTrace();
if (tx != null) {
tx.rollback();
}
}
}
说明: 以上代码仅设置了Dept对象的emp属性,由于元素的inverse属性值为true,Hibernate没有执行任何update语句,也就意味着Emp表中该条员工记录的外键没有得到正确更新,由此可得到以下操作建议:
-
当映射双向一对多的关联关系时,在“一”方把元素的inverse属性设置为true,可以提高应用的性能。
-
在代码中建立两个对象到的关联时,应该同时修改关联两端对象的相关属性。
emp.setDept(dept);
dept.getEmps().add(emp);
这样才能使程序更加健壮,提高业务逻辑层的独立性,使业务逻辑层的代码不受Hibernate实现的影响。同时,当解除双向关联的关系时,也应该同时修改关联两端的对象的相应属性,如下:
emp.setDept(null);
dept.getEmps().remove(emp);
四、建立多对多关联关系
1、配置单向多对多关联关系
Project.java
/**
* 项目类
*/
public class Project implements Serializable {
//项目编号
private Integer proid;
//项目名称
private String proName;
//参与项目的多个员工
private Set<Employee> employees=new HashSet<Employee>(0);
//省略getter/setter方法
}
Employee.java
/**
* 员工类
*/
public class Employee implements Serializable {
/**
* 员工编号
*/
private Integer empId;
/**
* 员工姓名
*/
private String empName;
}
Project.hbm.xml配置如下:
<hibernate-mapping>
<class name="cn.demo.po.Project" table="`PROJECT`" schema="scott" dynamic-update="true">
<id name="proid" column="PROID" type="java.lang.Integer"/>
<property name="proName" column="PRONAME" type="java.lang.String"/>
<!--映射Proect-Employee多对多关联关系-->
<set name="employees" table="PROEMP" cascade="save-update">
<!--ProEmp表的项目id-->
<key column="RPROID"/>
<!--集合类型为Employee-->
<many-to-many class="cn.demo.po.Employee" column="REMPID"/>
</set>
</class>
</hibernate-mapping>
说明:
- (1)元素的table属性指定关系表的名称为PROEMP
- (2)元素的cascade属性为“save-update”,表明保存或更新Project对象时,会级联保存或更新与它关联的Employee对象。
- (3)元素的子元素指定PROEMP的外键RPROID,用来参照Project表
- (4)子元素的class属性指定employees集合中存放的是Employee对象,
column属性指定PROEMP表的外键REMPID,用来参照Employee表。
注:对于多对多关联,cascade属性设置为"save-update"是合理的,但是不建议把cascade属性设为“all”“delete”。如果删除一个Project对象则级联删除与它关联的的所有Employee对象,由于这些Employee对象还有可能与其他Project对象关联,因此当Hibernate执行级联删除时,会破坏数据库的外键参照完整性。
ProjectDao.java
/**
* 添加项目
* @param project
*/
public void save(Project project){
this.currentSession().save(project);
}
ProjectBiz.java
/**
* 添加项目
* @param project
*/
public void addNewProject(Project project){
Transaction tx=null;
try {
tx=projectDao.currentSession().beginTransaction();
projectDao.save(project);
tx.commit();
}catch (HibernateException e){
e.printStackTrace();
if(tx!=null){
tx.rollback();
}
}
}
测试类
/**
* 测试添加项目
*/
@Test
public void testAddNewProject(){
//创建两个员工对象
Employee employee1=new Employee(1,"张三");
Employee employee2=new Employee(2,"李四");
//创建两个项目对象
Project project1=new Project(1,"1号项目");
Project project2=new Project(2,"2号项目");
//将项目1分别与两个员工进行关联
project1.getEmployees().add(employee1);
project1.getEmployees().add(employee2);
//将项目2与employee1进行关联
project2.getEmployees().add(employee1);
//进行持久化操作
projectBiz.addNewProject(project1);
projectBiz.addNewProject(project2);
}
说明:
- (1)当Session的save()方法保存project1对象时,向PROJECT表插入一条记录,同时还会分别向EMPLOYEE和PROEMP表插入两条记录。
- (2)当Session的save()方法保存project2对象时,向PROJECT插入一条记录,同时向PROEMP表插入一条记录。由于与project2对象关联的employee1对象已经被保存到数据库中,因此不再向EMPLOYEE表插入记录。
- (3)注意主键生成策略,不能为increment,只能为assigned。
2、配置双向多对多关联关系
在Employee.java中添加属性projects
/**
* 项目集合
*/
private Set<Project> projects=new HashSet<Project>();
public Set<Project> getProjects() {
return projects;
}
public void setProjects(Set<Project> projects) {
this.projects = projects;
}
Project.hbm.xml不做更改,配置多对多关联关系
<hibernate-mapping>
<class name="cn.demo.po.Project" table="PROJECT1" schema="scott">
<id name="proId" column="`PROID`" type="java.lang.Integer">
<generator class="assigned"/>
</id>
<property name="proName" column="`PRONAME`" type="java.lang.String"/>
<!--映射Proect-Employee多对多关联关系-->
<set name="employees" table="PROEMP" cascade="save-update">
<!--ProEmp表的项目id-->
<key column="RPROID"/>
<!--集合类型为Employee-->
<many-to-many class="cn.demo.po.Employee" column="REMPID"/>
</set>
</class>
</hibernate-mapping>
在Employee.hbm.xml中配置多对多关联关系
<hibernate-mapping>
<class name="cn.demo.po.Employee" table="EMPLOYEE" schema="scott">
<id name="empId" column="EMPID" type="java.lang.Integer">
<generator class="assigned"/>
</id>
<property name="empName" column="EMPNAME" type="java.lang.String"/>
<!--员工与项目之间的多对多关联关系-->
<set name="projects" table="PROEMP" inverse="true">
<key column="REMPID"/>
<!--指定projects集合类型-->
<many-to-many class="cn.demo.po.Project" column="RPROID"/>
</set>
</class>
</hibernate-mapping>
Dao层Service层不做修改,测试类如下
/**
* 测试添加项目:双向多对多关系
*/
@Test
public void testAddNewProject2(){
//创建两个员工对象
Employee employee1=new Employee(1,"张三");
Employee employee2=new Employee(2,"李四");
//创建两个项目对象
Project project1=new Project(1,"1号项目");
Project project2=new Project(2,"2号项目");
//建立项目到员工的关系
project1.getEmployees().add(employee1);
project1.getEmployees().add(employee2);
project2.getEmployees().add(employee1);
//建立员工到项目的关系
employee1.getProjects().add(project1);
employee1.getProjects().add(project2);
employee2.getProjects().add(project1);
//进行添加操作
projectBiz.addNewProject(project1);
projectBiz.addNewProject(project2);
}
注:在实际开发中,如果连接表中除了两个外键,还包括其他业务字段,那么根据业务需要,可以把多对多关联分解为两个一对多关联。
五、配置查询加载策略
通过关联关系可以在程序中方便的获取关联对象的数据,但是如果从数据库中加载Dept对象,会同时自动加载所有关联的Emp对象,而程序实际上仅仅需要访问Dept对象,那么这些关联的Emp对象就白白浪费了许多空间。当Hibernate查询Dept对象时,立即查询并加载与之关联的Emp对象,这种查询策略称为立即加载。立即加载存在两大不足:
- (1)会执行不必要的查询语句,影响查询性能。
- (2)可能会加载大量不需要的对象,增加系统开销,浪费内存空间。
为了解决以上问题,Hibernate提供了延迟加载策略。
延迟加载策略能避免加载应用程序不需要访问的关联对象。
Hibernate允许在对象-关系映射文件中使用lazy属性配置加载策略,并且可以分为类级和关联级两个级别分别进行控制。lazy属性常用取值如下:
-
(1)类级别:元素中lazy的属性可选值为true(延迟加载)和false(立即加载),默认true。
-
(2)一对多关联级别:元素中lazy属性的可选值为true(延迟加载)、extra(增强延迟加载)和false(立即加载),默认true。
-
(3)多对一关联级别:元素中lazy属性的可选值为proxy(延迟加载)、no-proxy(无代理延迟加载)和false(立即加载)。默认proxy。
说明:由此可看出,对于Hibernate3.x以上的版本,无论哪个级别,默认采用的都是延迟加载的查询策略,以减少系统资源的开销。
1、配置类级别的查询加载策略
类级别可选的加载策略包括立即加载和延迟加载,默认为延迟加载。如果元素的lazy属性为true,表示采用延迟加载;如果lazy属性为false,表示采用立即加载。
(1)立即加载
例:
<class name="cn.demo.po.Dept" lazy="false" table="DEPT">...</class>
说明:当通过Session的load()方法加载Dept对象时,执行语句后:
Dept dept=(Dept)session.load(Dept.class,10);
Hibernate会立即执行查询Dept表中的select语句:
select * from dept where deptno=?
(2)延迟加载
类级别的默认加载策略是延迟加载,以下两种方式都表示采用延迟加载策略:
<class name="cn.demo.po.Dept" table="DEPT"></class>
或
<class name="cn.demo.po.Dept" lazy="true" table="DEPT"></class>
说明:如果程序加载一个持久化对象的目的是访问它的属性,则可以采用立即加载。如果程序加载一个持久化对象的目的仅仅是获得它的引用,则可以采用延迟加载。
范例:
@Test
public void testAddNewEmp(){
Transaction tx=null;
try {
tx=empDao.currentSession().beginTransaction();
//使用session.load方法延迟加载dept对象
Dept dept=(Dept) empDao.currentSession().load(Dept.class,10);
Emp emp=new Emp();
emp.setEname("TOM");
emp.setDept(dept);
empDao.currentSession().save(emp); //调用Session的save方法进行保存
tx.commit();
}catch (HibernateException e){
e.printStackTrace();
if(tx!=null){
tx.rollback();
}
}
}
说明: 因为Dept类级别采用延迟加载,session.load()方法不会执行访问Dept表的select语句,只会返回一个Dept代理类的实例,它的deptNo属性为10,其余属性都为null。以上代码仅由session.save()方法执行一条insert语句:
insert into scott.EMP (ENAME, MGR, JOB, SAL, COMM, HIREDATE,
DEPTNO, EMPNO)
values (?, ?, ?, ?, ?, ?, ?, ?)
当元素的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代理类实例。
tx=session.beginTransaction();
Dept dept=(Dept)session.load(Dept.class,10);
//判断是否已经初始化,若没有则显式初始化
if(!Hibernate.isInitialized(dept)){
Hibernate.initialize(dept);
}
- (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()方法在类级别总是使用立即加载策略,
举例说明如下:
【1】当通过Session的get()方法加载Dept对象时:
tx=session.beginTransaction();
Dept dept=(Dept)session.get(Dept.class,10);
tx.commit();
Hibernate会立即执行以下select语句:select * from dept where deptno=?
如果存在相关的数据,get()方法就会返回Dept对象,否则就返回null。
get()方法永远不会返回Dept代理类实例,这是它与load()方法的区别之一。
【2】当运行Query对象的list()方法时:
tx=session.beginTransaction();
List deptList=session.createQuery("from Dept").list();
tx.commit();
Hibernate会立即执行以下select语句:select * from dept
2、配置一对多和多对多关联的查询加载策略
在映射文件中,用元素的lazy属性来配置一对多及多对多关联关系的加载策略。Dept.hbm.xml文件中的如下代码配置了Dept类Emp类的一对多映射关系。
<set name="emps" inverse="true" lazy="true">
<key column="DEPTNO"/>
<one-to-many class="cn.demo.po.Emp"/>
</set>
说明:元素的lazy属性的取值决定了emps集合被初始化的时机,即到底是在加载Dept对象时就被初始化,还是在程序访问emps集合时被初始化。<set>
元素的lazy取值如下:
- true:默认值,延迟加载
- false:立即加载
- extra:增强延迟加载
(1)立即加载
如何设置: <set name="emps" inverse="true" lazy="false">...</set>
例:
tx=session.beginTransaction();
Dept dept=(Dept)session.get(Dept.class,10);
tx.commit();
说明:执行Session的get()方法时,对于Dept对象采用类级别的立即加载策略;对于Dept对象的emps集合(即与Dept关联的所有Emp对象)采用一对多关联级别的立即加载策略。因此Hibernate会执行以下select语句:
select * from dept where deptno=? //根据部门ID查询指定部门
select * from emp where DEPTNO=? //根据部门ID获取Emp对象
通过以上select语句,Hibernate加载了一个Dept对象和多个Emp对象。在很多情况下,应用程序并不需要访问这些Emp对象,所以在一对多关联级别中不能随意使用立即加载策略。
(2)延迟加载
对于<set>
元素,应该优先考虑使用默认的延迟加载策略。
如何设置:
<set name="emps" inverse="true"></set>
或
<set name="emps" inverse="true" lazy="true"></set>
再次运行以下代码,仅立即加载Dept对象,仅执行select语句:
select * from dept where deptno=?
tx=session.beginTransaction();
Dept dept=(Dept)session.get(Dept.class,10);
tx.commit();
说明: Session的get()方法的Dept对象中,emps属性引用一个没有被初始化的集合代理类实例。换句话说:此时emps集合中没有存放任何Emp对象。只有当emps集合代理类实例被初始化时,才会到数据库中查询所有与Dept关联的Emp对象。
Dept对象的emps属性引用的集合代理类实例何时初始化:两种情况
- 【1】会话关闭前,应用程序第一次访问它时,如调用它的iterator()、size()、isEmpty()或contains()方法。
Set<Emp> emps=dept.getEmps();
//导致emps集合代理类实例被初始化
Iterator<Emp> empIterator=emps.iterator();
- 【2】会话关闭前,通过org.hibernate.Hibermate类的initialize()静态方法初始化。
Set<Emp> emps=dept.getEmps();
//导致emps集合代理类实例被初始化
Hibernate.initialize(emps);
(3)增强延迟加载
在<set>
元素中配置lazy属性为"extra",表明采用增强延迟加载策略,如:
<set name="emps" inverse="true" lazy="extra"></set>
说明: 增强延迟加载策略与一般的延迟加载策略(lazy=“true”)的主要区别在于,增强延迟加载策略能进一步延迟Dept对象的emps集合代理类实例的初始化时机。当程序第一次访问emps属性的iterator()方法时,会导致emps集合代理类实例的初始化,而当程序第一次访问emps属性的size()、contains()和isEmpty()方法时,Hibernate不会初始化emps集合代理实例,仅通过特定的select语句查询必要的信息。
例1:以下语句不会初始化emps集合代理类实例
tx=session.beginTransaction();
Dept dept=(Dept)session.get(Dept.class,10);
//执行SQL语句:select count(empno) from emp where DEPTNO=?
dept.getEmps().size();
例2:
//以下语句会初始化emps集合代理类实例
dept.getEmps().iterator();
tx.commit();
3、配置多对一关联的查询加载策略
在映射文件中,元素用来设置多对一关联关系。在Emp.hbm.xml文件中,以下代码设置Emp类与Dept类的多对一关联关系:
<many-to-one name="dept" column="DEPTNO" lazy="proxy"
class="cn.demo.po.Dept"/>
元素的lazy属性设置不同取值时的加载策略如下:
- proxy:默认值,延迟加载
- no-proxy:无代理延迟加载
- false:立即加载
如果没有显式设置lazy属性,那么在多对一关联级别采用默认的延迟加载策略。假如应用程序仅仅希望访问Emp对象,并不需要立即访问与Emp关联的Dept对象,则应该使用默认的延迟加载策略。
(1)延迟加载
在元素中配置lazy属性为“proxy”,延迟加载与Emp关联的Dept对象。
例:
tx=session.beginTransaction();
Emp emp=(Emp)session.get(Emp.class,7839);
//emp.getDept() 返回Dept代理类实例的应用
Dept dept=emp.getDept();
dept.getDeptName();
tx.commit();
说明:
当运行Session的get()方法时,仅立即执行查询Emp对象的select语句:
select * from Emp where empno=?
Emp对象的dept属性引用Dept代理类实例,这个代理类实例的OID由EMP表的DEPTNO外键值决定。当执行dept.getDeptName()方法时,Hibernate初始化Dept代理类实例,执行以下select语句从数据库中加载Dept对象:
select * from dept where deptno=?
(2)无代理延迟加载
在元素中配置lazy属性为“no-proxy”,如下
<many-to-one name="dept" column="DEPTNO" lazy="no-proxy" class="cn.demo.po.Dept"/>
对于以下代码:
tx=session.beginTransaction();
Emp emp=(Emp)session.get(Emp.class,7839); //第1行
Dept dept=emp.getDept(); //第2行
dept.getDeptName(); //第3行
tx.commit();
说明:
-
如果对Emp对象的dept属性使用无代理延迟加载,即元素的lazy属性为“no-proxy”,那么程序第一行加载的Emp对象的dept属性为null。
当程序第二行调用emp.getDept()方法时,将触发Hibernate执行查询DEPT表的selec语句,从而加载Dept对象。 -
由此可见,当lazy属性为“proxy”时,可以延长延迟加载Dept对象的时间。而当lazy属性为“no-proxy”时,则可以避免使用由Hibernate提供的Dept代理类实例,使Hibernate对程序提供更加透明的持久化服务。
-
注意:当lazy属性为“no-proxy”时,需要在编译期间进行字节码增强操作,否则运行情况和lazy属性为“proxy”时相同,因此很少用到。
(3)立即加载
以下代码把Emp.hbm.xml文件中<many-to-one>
元素的lazy属性设置为false:
<many-to-one name="dept" column="DEPTNO" lazy="false" class="cn.demo.po.Dept"/>
对于以下程序代码:
tx=session.beginTransaction();
Emp emp=(Emp)session.get(Emp.class,7839);
tx.commit();
在运行session.get()方法时,Hibernate会执行以下select语句:
select * from emp where empno=? //根据empno查询员工
select * from dept where deptno=? //根据查询的员工对象中的deptno查部门
4、配置OpenSessionInView模式
在Java Web应用中,通常需要调用Hibernate API获取要显示的某个对象并传给相应的视图JSP,并在JSP中根据需要通过这个对象导航到与之关联的对象或集合数据。这些关联对象或集合数据如果是被延迟加载的,且在执行完查询后Session对象已经关闭,Hibernate就会抛出LazyInitializationException异常。针对这一问题,Hiber社区提出了Open Session In View模式作为解决方案。这个模式的主要思想是:在用户的每一次请求过程中,始终保持一个Session对象处于开启状态。
Open Session In View模式的具体实现有以下三个步骤:
(1)第一步: 把Session绑定到当前线程上,要保证在一次请求中只有一个Session对象,Dao层的HibernateUtil.currentSession()方法使用SessionFactory的getCurrentSession()方法获得Session,可以保证每一次请求的处理线程上只有一个Session对象存在。
(2)第二步: 用Filter过滤器在请求到达时打开Session,在页面生成完毕时关闭Session。代码如下:
OpenSessionInViewFilter.java
package cn.demo.utils;
import org.hibernate.HibernateException;
import org.hibernate.Transaction;
import javax.servlet.*;
import java.io.IOException;
public class OpenSessionInViewFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
Transaction tx=null;
try {
//请求到达时,打开Session并启动事务
tx=HibernateUtil.currentSession().beginTransaction();
//执行请求处理链
filterChain.doFilter(servletRequest,servletResponse);
//返回响应时,提交事务
tx.commit();
}catch (HibernateException e){
e.printStackTrace();
if(tx!=null){
tx.rollback(); //回滚事务
}
}
}
}
web.xml中配置过滤器
<webapp>
<!--配置OpenSessionInView-->
<filter>
<filter-name>openSessionInView</filter-name>
<filter-class>cn.bdqn.utils.OpenSessionInViewFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>openSessionInView</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</webapp>
说明:每一次请求都在OpenSessionInViewFilter过滤器中打开Session,开启事务;页面生成完毕之后,在OpenSessionInViewFilter过滤器中结束事务并关闭Session。
(3)第三步: 调整业务层代码,删除和会话及事务管理相关的代码,仅保留业务逻辑代码。
DeptBizImpl.java代码如下:
public class DeptBizImpl implements DeptBiz{
private DeptDao deptDao=new DeptDao();
@Override
public Dept findDeptByDeptNo(Integer deptNo)throws Exception{
return this.deptDao.findById(deptNo);
}
}
数据访问层代码风格不变。
小结:在OpenSessionInViewFilter过滤器中获取Session对象,保证一次请求过程中始终使用一个Session对象。视图JSP从一个对象导航到与之关联的对象或集合,即使这些对象或集合是被延迟加载的,因为当前Session对象没有关闭,所以能顺利的获取到关联当对象或集合的数据。直到视图JSP数据全部响应完成,OpenSessionInViewFilter过滤器才结束事务并关闭Session。