SSH Chapter 06 Hibernate 关联映射

SSH Chapter 06 Hibernate 关联映射 笔记

本章目标:

  • 理解Hibernate的关联映射
  • 理解inverse属性、cascade属性
  • 掌握单向的多对一、双向的一对多映射
  • 掌握多对多映射
  • 掌握延迟加载

技术内容:

前面我们已经掌握了如何配置Hibernate对数据库进行,,,操作,掌握Java对象的生命周期.

在学习面向对象时,学习过对象之间存在关联的关系.

在学习数据库时,也学习过表和表之间也可以通过外键关联起来.

怎样映射面向对象领域的关联关系和数据库关系中的外键关联呢?

这是本章要关注的问题.

1 . 关联关系

类与类之间最普遍的关系就是关联关系,而且关联是有方向的。

**以部门(Dept)和员工(Emp)为例,一个部门下有多个员工,而一个员工只能属于一个部门. **

EmpDept的关联是多对一关联,这就意味着每个Emp对象只能引用一个Dept对象;

**而从DeptEmp是一对多关联,这意味着每个Dept对象会引用一组Emp对象; **

因此,在Emp类中应该定义一个Dept类型的属性,来引用所关联的Dept对象;而在Dept类中应该定义一个集合类型的属性,来引用所有关联的Emp对象

如果仅有从EmpDept的关联,或者仅有从DeptEmp的关联,就称为单向关联.如果同时包含两种关联,就称为双向关联,如图:
在这里插入图片描述
关联关系是使用最多的一种关系,非常重要。在内存中反映为实体关系,映射到DB中为主外键关系。

实体间的关联,即对外键的维护。关联关系的发生,即对外键数据的改变。

外键:外面的主键,即,使用其它表的主键值作为自已的某字段的取值。

本章将结合具体案例来介绍如何映射以下关联关系:

(1)以Emp类和Dept类为例 , 介绍如何映射多对一单向关联关系

(2)以Emp类和Dept类为例 , 介绍如何映射一对多双向关联关系

(3)以Project类和Employee类为例 , 介绍如何映射多对多关联关系

2 . 建立单向多对一关联

首先以EmpDept为例介绍如何建立单向多对一关联关系.

2.1 配置单向多对一关联

Emp类中需要定义一个Dept属性,而在Dept类中无须定义用于存放Emp对象的集合属性,示例1示例2分别是DeptEmp持久化类.

示例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表的外键DEPTNOdept属性之间的映射.它包括以下属性:

(1) name: 设定持久化类的属性名,此处为Emp类的dept属性.

(2) column: 设定持久化类的属性对应的表的外键,此处为EMP表的外键DEPTNO

(3) class: 设定持久化类的属性的类型,此处设定dept属性为Dept类型

至此 , Emp 类到Dept类的单向多对一映射就完成了.

2.2 实现持久化操作

Dept类,Emp类及其映射文件已经编写完成 , 从EmpDept的单向多对一关联已经建立.在这个基础上,实现具有关联关系的对象的持久化.

(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=:deptNoHQL语句实现,直接传递部门编号作为查询条件

(3) 查询Emp关联的Dept信息

输出指定Emp集合中所有Emp对象及其关联的Dept对象的信息.

由于EmpDept之间存在着单向多对一关系 , 所以只要调用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对象关联,所以必须通过HibernateAPI 查询数据库:

String hql = "from Emp where dept.deptNo = 10";
List<Emp> list = session.createQuery(hql).list();

在使用面向对象语言编写的程序中 , 通过关联关系从一个对象导航到另一个对象显然比通过编码到数据库查询来得更加自然 , 且无须额外的编码 .

并且 , 基于关联关系 , 在,,操作中还可以对相关对象实现自动化的==级联处理== , 同时减少了编码工作量 , 提高了开发效率 . 因此不妨为Dept类和Emp类建立双向一对多关联.

3.1 配置双向一对多关联

在前面的示例中已经建立了Emp类到Dept类的多对一关联 , 下面再增加DeptEmp类的一对多关联 , 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属性设定所关联的持久化类型 , 此处为EmpHibernate 通过以上的映射代码 , 可获得一下信息:

  • <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类及其映射文件已经编写完成 , 从DeptEmp的双向一对多关联已经建立 , 下面来完成以下持久化操作:

  1. 先创建Dept对象 , 然后创建一个Emp对象 , 将这两个对象进行关联 . 最后保存Dept对象 , 同时自动保存这个Emp对象.
  2. 删除Dept对象 , 并级联删除与Dept对象关联的Emp对象

要完成以上两个持久化操作 , 需要在<set>元素中配置cascade属性.

在对应-关系映射文件中 , 用于映射持久化类之间关联关系的元素 , 如<set>,<many-to-one>都有一个cascade属性 . 它用于指定如何操纵与当前对象关联的其他对象 .

如下表列出了cascade属性的部分常用可选值:

cascade属性值描 述
noneSession操纵当前对象时,忽略其他关联的对象。它是cascade属性的默认值
save-update当通过Sessionsave()update()saveOrUpdate()方法来保存或更新当前对象时,级联保存所有关联的新建的瞬时状态的对象,并且级联更新所有关联的游离状态的对象
merge当通过Sessionmerge()方法来保存或更新当前对象时,对其关联对象也执行merge()方法
delete当通过Sessiondelete()方法删除当前对象时,会级联删除所有关联的对象
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属性的值有两个 , 即truefalse , 默认是false, 关联关系中, inverse="false"的为主动方 ,inverse="true"的为被动方, 主动方会负责维护关联关系 .

注意:若一的一方不负责维护关联关系,则需要使用代码建立多的一方到一的一方之间的关联关系,否则多的一方的外键在级联新增操作时不会更新

例如示例10中的Dept一方 , 会主动执行执行update语句 : update emp set DEPTNO=? where empmo=?, 以维护外键的取值 .

示例12演示了<set>元素的inverse属性不同取值的影响 .

示例12中先加载持久化类DeptEmp对象 , 然后双向建立两者的关联关系 , 实现调整员工所属的部门 .

示例12:

EmpDaoDeptDao中添加根据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>

以上配置表明在DeptEmp双向关联关系中 , 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) 修改示例12EmpBiz.java,**不建立DeptEmp对象的关联 **, 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) 修改示例12EmpBiz.java,**不建立EmpDept对象的关联 **, 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表中该员工记录的外键没有得到正确的更新 . 由此可以得到如下操作建议:

  1. 映射双向一对多的关联关系时 , 在"一"方把<set>元素的inverse属性设置为true,可以提高应用的性能.
  2. 在代码中建立两个对象的关联时 , 应该同时修改两个关联对象的相关属性:
    • 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);
}

Sessionsave()方法保存project1对象时 , 向PROJECT表插入一条记录 , 同时还会分别向EMPLOYEEPROEMP表插入两条记录 , 执行如下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 (?, ?)

Sessionsave()方法保存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 . 使用双向多对多关联完成持久化操作 , 同时建立从ProjectEmployee 和从EmployeeProject的关联关系 如示例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

由上表可以看出 , 对于Hibernate3.x以上的版本 , 无论哪个级别 , 默认采用的都是延迟加载的查询策略 , 以减少系统资源的开销 .

5.1 类级别的查询策略:

类级别可选的加载策略包括立即加载和延迟加载。默认为延迟加载。如果<class>元素的lazy属性为true。表示采用延迟加载;如果lazy属性为false,表示采用立即加载.

以Emp和Dept为例:

1 . 立即加载

在Dept.hbm.xml中的<class>元素中添加属性 lazy="false" 表示立即加载

<class name="Dept" table="DEPT" lazy="false">

当测试方法 通过Sessionload()方法加载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所示 , 示例15Dept类级别采用延迟加载 , 向数据库保存了一个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时 , 会影响Sessionload()方法的各种运行时行为 , 下面举例说明:

(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在数据库中不存在 , Sessionload()方法不会立即抛出异常 , 因为此时并未真正执行查询 . 只有当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 , Sessionget()方法以及Query对象的list()方法在类级别总是使用立即加载策略 , 举例说明如下:

  • 当通过Sessionget()方法加载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: 通过Sessionget()方法加载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();

执行Sessionget()方法时 , 对于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=?

Sessionget()方法返回的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属性引用集合代理类实例何时初始化呢 ? 主要包括以下两种情况:

  1. 会话关闭前 , 应用程序第一次访问它时 , 如调用它的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();
    
  2. 会话关闭前 , 通过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对象 , 这是Sessionget()方法起了作用.而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加载数据,我们就希望这时的sessionopen着的,这就是所谓的Open Session In view

其实比较好的理解就是,关于Session关闭的问题,如果我们在数据访问层关闭了Session,那么我们在业务逻辑层想要获取,延迟加载的数据时,就会抛出异常。

相对应的如果业务逻辑层关闭Session,那么也会在表示层获取数据的时候出现异常。

所以我们的解决方法就是当访问网页的时候,请求开始和响应结束的时候,使用同一个Session

而这个模式的主要思想是 : 在用户每次请求过程中 , 始终保持一个Session对象处于开启状态。
在这里插入图片描述
Open Session In View模式的具体实现有以下三个步骤:

第一步 :

Session绑定到当前线程上 , 要保证在一次请求中只有一个Session对象 . DaoHibernateUtil.currentSession()方法使用SessionFactorygetCurrentSession()方法获得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

  1. 首先从 jetty 官方网站下载最新的 jetty的zip包,官网地址:https://www.eclipse.org/jetty/download.html
  2. 将下载的压缩包解压到指定目录,比如: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 来进行启动。

  1. 运行 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 部署项目

  1. 将自己的项目复制到 jetty 目录的 webapps 目录下。在这里插入图片描述
  2. 在jetty目录下创建work目录,如图所示:在这里插入图片描述
  3. 开始部署项目,运行 java -jar start.jar
  4. 打开浏览器访问 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/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值