Hibernate 03-配置关联关系映射

一、了解关联关系

  • 类与类之间最普遍的关系就是关联关系,并且关联是有方向的。以部门(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。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值