Hibernate关联映射(非常详细)

 
在前面的学习中,我们所涉及的都是基于单表的操作,但在实际的开发过程中,基本上都是同时对多张表的操作,且这些表都存在一定的关联关系。

Hibernate 是一款基于 ORM 设计思想的框架,它将关系型数据库中的表与我们 Java 实体类进行映射,表中的记录对应实体类的对象,而表中的字段对应着实体类中的属性。Hibernate 进行增删改查等操作时,不再直接操作数据库表,而是对与之对应的实体类对象进行处理。那么,Hibernate 是如何处理多表关联问题的呢?本节我们针对此问题进行介绍。

关联映射

在关系型数据库中,多表之间存在着三种关联关系,分别为一对一、一对多和多对多,如图 1 所示。

Hibernate 关联关系映射
图1:多表关联关系

关联关系描述的是数据库表之间的引用关系,而 Hibernate 关联映射指的就是与数据库表对应的实体类之间的引用关系。

在数据库中,如果两张表想要建立关联关系,就需要外键来连接它们,数据库表之间的关系是没有方向性的,且彼此是透明的。而在 Java 中,如两个类想要建立关系的话,那就需要这两个类都通过属性(变量)来管理对方的引用,以达到建立关联关系的目的,Hibernate 关联映射也是通过这种方式实现的。

一对多

在三种关联关系中,一对多(或者多对一)是最常见的一种关联关系。

在关系型数据库中,一对多映射关系通常是由“多”的一方指向“一”的一方。在表示“多”的一方的数据表中增加一个外键,指向“一”的一方的数据表的主键,“一”的一方称为主表,而“多”的一方称为从表。

图2:数据库映射一对多关联关系

图 1 说明如下:
  • student 表为学生表,id 为学生表的主键,name 表示学生名称;
  • grade 表为班级表,id 为班级表的主键,name 表示班级名称;
  • gid 为学生表的外键,指向班级表的主键 id;

使用 Hibernate 映射“一对多”关联关系时,需要如下步骤:
  • 在“多”的一方的实体类中,引入“一”的一方实体类对象作为其属性,并在映射文件中通过 <many-to-one> 标签进行映射;
  • 在“一”的一方的实体类中,以 Set 集合的形式引入“多”的一方实体类对象作为其属性,并在映射文件中通过 <set> 标签进行映射。

学生和班级之间的关系,是典型的一对多关联关系,一个学生只能属于一个班级,而一个班级可以有多个学生,下面我们以学生(Student)和班级(grade)为例,演示如何使用 Hibernate 建立一对多关联映射。

创建实体类

1. 在 net.biancheng.www.po 包中,创建一个名为 Student 的实体类,代码如下。
package net.biancheng.www.po;

public class Student {
    private Integer id;
    private String name;
    //持有实体类 Grade 的一个引用,维护多对一关系
    private Grade grade;

    public Student() {
    }

    public Student(Integer id, String name) {
        this.id = id;
        this.name = name;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Grade getGrade() {
        return grade;
    }

    public void setGrade(Grade grade) {
        this.grade = grade;
    }

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", grade=" + grade +
                '}';
    }
}

2. 在 net.biancheng.www.po 包中,创建一个名为 Grade 的实体类,代码如下。
package net.biancheng.www.po;

import java.util.HashSet;
import java.util.Set;

public class Grade {
    private Integer Id;
    private String name;
    //持有 Student 引用的集合,来维护一对多关联关系
    private Set<Student> students = new HashSet<>();

    public Integer getId() {
        return Id;
    }

    public void setId(Integer id) {
        Id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Set<Student> getStudents() {
        return students;
    }

    public void setStudents(Set<Student> students) {
        this.students = students;
    }

    @Override
    public String toString() {
        return "Grade{" +
                "Id=" + Id +
                ", name='" + name +
                '}';
    }
}

建立映射

1. 在 net.biancheng.www.mapping 包中,创建 Student 的映射文件 Student.hbm.xml,配置如下。
<?xml version='1.0' encoding='utf-8'?>
<!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="net.biancheng.www.po.Student" table="student" schema="bianchengbang_jdbc" lazy="true">
        <!--主键映射-->
        <id name="id" column="id" type="java.lang.Integer">
            <!--主键生成策略-->
            <generator class="native"></generator>
        </id>
        <property name="name" column="name" length="100" type="java.lang.String"></property>

        <!--维护关联关系-->
        <many-to-one name="grade" class="net.biancheng.www.po.Grade" column="gid"/>
    </class>
</hibernate-mapping>

从 Student(学生)的角度看,Student 和 Grade 的关系为多对一,因此 Student 映射文件需要通过<many-to-one> 来维护 Student 与 Grade 的多对一关联关系。

<many-to-one> 标签通常会包含以下常用属性,如下表。

属性名 描述
name 用来设置关联对象的属性名称
column 用来设置实体类所对应的数据库表的外键的字段名称
class 用来设置关联对象类的完全限定名(包名+类名)


2. 在 net.biancheng.www.mapping 包中,创建 Grade 的映射文件 Grade.hbm.xml,配置如下。
<?xml version='1.0' encoding='utf-8'?>
<!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="net.biancheng.www.po.Grade" table="grade" schema="bianchengbang_jdbc">
        <id name="id" column="id" type="java.lang.Integer">
            <generator class="native"></generator>
        </id>
        <property name="name" column="name" length="100" type="java.lang.String"/>
        <!--使用 set 元素维护一对多关联关系-->
        <set name="students">
            <key column="gid"></key>
            <one-to-many class="net.biancheng.www.po.Student"></one-to-many>
        </set>
    </class>
</hibernate-mapping>

从 Grade(班级)的角度看,Student 和 Grade 是一对多的关系,因此 Grade 的映射文件中,需要通过 <set> 标签来维护 Grade 与 Student 的一对多关联关系。

<set> 标签中通常会包含以下属性和子标签,如下表。

名称 类型 描述
name 属性 关联对象引用(集合类型)的属性名称;
<key> 子标签 用于设置关联对象所对应的数据库表的外键,其 colum 属性用于指定外键的字段名称。
<one-to-many>  子标签 用于维护一对多关系,其 class 属性用来设置关联对象类的完全限定名(包名+类名)。

5. 在 Hibernate 核心配置文件 hibernate.cfg.xml 中,使用 <mapping> 元素来指定映射文件 Student.hbm.xml 和 Grade.hbm.xml 的位置信息,配置代码如下。
<mapping resource="net/biancheng/www/mapping/Grade.hbm.xml "/>
<mapping resource="net/biancheng/www/mapping/Student.hbm.xml" />

测试

1. 在测试类 MyTest 中,添加一个 addCascade() 方法,代码如下。
/**
* 一对多
*/
@Test
public void addCascade() {
    Session session = HibernateUtils.openSession();
    Transaction transaction = session.beginTransaction();
    //创建一个班级对象
    Grade grade = new Grade();
    grade.setName("一年级");
    //创建学生对象
    Student student = new Student();
    student.setName("小明");
    //设置学生的班级
    student.setGrade(grade);
    //创建学生对象
    Student student2 = new Student();
    student2.setName("小红");
    //设置学生的班级
    student2.setGrade(grade);
    //设置班级内有哪些学生
    grade.getStudents().add(student);
    grade.getStudents().add(student2);

    //保存学生信息
    session.save(student);
    session.save(student2);
    //保存班级信息
    session.save(grade);
    //提交事务
    transaction.commit();
    //释放资源
    session.close();
}

2. 执行该测试方法,控制台输出如下。
Hibernate:
   
    create table grade (
       id integer not null auto_increment,
        name varchar(100),
        primary key (id)
    ) engine=MyISAM
Hibernate:
   
    create table student (
       id integer not null auto_increment,
        name varchar(100),
        gid integer,
        primary key (id)
    ) engine=MyISAM

Hibernate:
   
    alter table student
       add constraint FKrahcsicxucn7bhee8empkb42c
       foreign key (gid)
       references grade (id)

Hibernate:
    insert
    into
        grade
        (name)
    values
        (?)

Hibernate:
    insert
    into
        student
        (name, gid)
    values
        (?, ?)

Hibernate:
    insert
    into
        student
        (name, gid)
    values
        (?, ?)

Hibernate:
    update
        student
    set
        gid=?
    where
        id=?

Hibernate:
    update
        student
    set
        gid=?
    where
        id=?

从控制台的输出可以看到,除了建表操作外,数据库还执行了 3 条 insert 语句和 2 条 update 语句。

3. 查询数据库表 student 中的数据,结果如下。
id name gid
1 小明 1
2 小红 1

4. 查询数据库表 grade 中的数据,结果如下。
id name
1 一年级

多对多

在实际的应用中,“多对多”也是一种常见的关联关系,例如学生和课程的关系,一个学生可以选修多门课程,一个课程可以被多名学生选修。

在关系型数据库中,是无法直接表达“多对多”关联关系的,我们一般会采用新建一张中间表,将一个“多对多”关联拆分为两个“一对多”关联解决此问题,如下图。


图3:多对多:数据库表设计方案
图 1 说明如下:
  • student 表为学生表,id 为学生表的主键,name 表示学生名称;
  • course 表为课程表,id 为课程表的主键,name 表示课程名称;
  • student_course 表为中间表,其中字段 cid 和 sid 是中间表的两个外键,分别指向 student 表和 course 表的主键 id;
  • 在 student_course 表中,sid 和 cid 是该表的联合主键。

Hibernate 在映射“多对多”关联关系时,需要在两个实体类中,分别以 Set 集合的方式引入对方的对象,并在映射文件中通过 <set> 标签进行映射。

下面我们以学生(Student)和课程(Course)为例,演示如何使用 Hibernate 建立多对多关联映射。

创建实体类

1. 在 net.biancheng.www.po 包下创建一个名为 Course 的实体类,并以 Set 集合的形式引入 Student 对象作为其属性,来维护 Course与 Student 之间的多对多关联关系,代码如下。
package net.biancheng.www.po;

import java.util.HashSet;
import java.util.Set;

/**
* 课程实体类
*/
public class Course {
    private Integer id;
    private String name;
    //学生 Student 的集合作为其属性,维护多对多关联关系
    private Set<Student> students = new HashSet<>();


    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Set<Student> getStudents() {
        return students;
    }

    public void setStudents(Set<Student> students) {
        this.students = students;
    }

    @Override
    public String toString() {
        return "Course{" +
                "id=" + id +
                ", name='" + name +
                '}';
    }
}

2. 修改 Student 类的代码,在 Student 类中以 Set 的形式引入 Course 对象作为其属性,来维护 Student 与 Course 之间的多对多关联关系,代码如下。
package net.biancheng.www.po;

import java.util.HashSet;
import java.util.Set;

/**
* 学生实体类
*/
public class Student {
    private Integer id;
    private String name;
    private Grade grade;
    //将 Course 对象的集合作为其属性,以维护它们之间的多对多关联关系
    private Set<Course> courses = new HashSet<>();

    public Student() {
    }

    public Set<Course> getCourses() {
        return courses;
    }

    public void setCourses(Set<Course> courses) {
        this.courses = courses;
    }

    public Student(Integer id, String name) {
        this.id = id;
        this.name = name;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Grade getGrade() {
        return grade;
    }

    public void setGrade(Grade grade) {
        this.grade = grade;
    }

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", grade=" + grade +
                '}';
    }
}

建立映射

1. 在 net.biancheng.www.mapping 包中,创建 Course 的映射文件 Coures.hbm.xml,具体配置如下。
<?xml version='1.0' encoding='utf-8'?>
<!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="net.biancheng.www.po.Course" table="course" schema="bianchengbang_jdbc">
        <id name="id" column="id" >
            <generator class="native"></generator>
        </id>

        <property name="name" column="name" length="100"/>
       
        <set name="students" table="student_course" cascade="save-update">
            <key column="cid"></key>
            <many-to-many class="net.biancheng.www.po.Student" column="sid"></many-to-many>
        </set>
    </class>
</hibernate-mapping>

2. 修改 Student.hbm.xml 的配置,在其中添加 <set> 标签映射关联关系,配置如下。
<?xml version='1.0' encoding='utf-8'?>
<!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="net.biancheng.www.po.Student" table="student" schema="bianchengbang_jdbc" lazy="true">
        <!--主键映射-->
        <id name="id" column="id" type="java.lang.Integer">
            <!--主键生成策略-->
            <generator class="native"></generator>
        </id>
        <property name="name" column="name" length="100" type="java.lang.String"></property>

        <!--维护关联关系-->
        <many-to-one name="grade" class="net.biancheng.www.po.Grade" column="gid"/>
        <set name="courses" table="student_course" lazy="false">
            <key column="sid"></key>
            <many-to-many class="net.biancheng.www.po.Course" column="cid"></many-to-many>
        </set>
    </class>
</hibernate-mapping>

Hibernate 在映射文件中,通过 <set> 标签来映射“多对多”关联关系,其常用属性和子标签说明如下表。

名称 类型 说明
name 属性 关联对象的名称
table 属性 中间表的表明
lazy 属性 是否启用懒加载
<key> 子标签 <set> 标签的子标签,用于映射中间表的外键字段;其 colum 属性,用于指定中间表的哪个外键指向当前数据库表的主键。
<many-to-many> 子标签 <set> 标签的子标签,用于关联实体类,该子标签包含以下属性:
  • class 属性:用于指定关联实体类的完全限定名(包名+类名);
  • column 属性:用于指定中间表的哪个外键指向关联数据库表的主键。

将映射文件加入核心配置文件

在 Hibernate 核心配置文件 hibernate.cfg.xml 中,使用 <mapping> 元素来指定映射文件 Course.hbm.xml 的位置信息。
<!--指定课程的映射文件-->
<mapping resource="net/biancheng/www/mapping/Course.hbm.xml" />

测试

1. 在测试类 MyTest 中,添加一个 testManyToManySave() 方法,代码如下。
/**
* 多对多
* 保存操作
*/
@Test
public void testManyToManySave() {
    Session session = HibernateUtils.openSession();
    Transaction transaction = session.beginTransaction();
   
    //新建学生和班级信息
    Grade grade = new Grade();
    grade.setName("三年级");
    Student student = new Student();
    student.setName("选课学生1");
    student.setGrade(grade);
    Student student2 = new Student();
    student2.setName("选课学生2");
    student2.setGrade(grade);
    grade.getStudents().add(student);
    grade.getStudents().add(student2);
   
    //新建三个课程
    Course course = new Course();
    course.setName("Java");
    Course course2 = new Course();
    course2.setName("PHP");
    Course course3 = new Course();
    course3.setName("C++");

    //学生选课
    course.getStudents().add(student);
    course.getStudents().add(student2);
    course3.getStudents().add(student);
    course3.getStudents().add(student2);
   
    //保存操作
    session.save(student);
    session.save(student2);
    session.save(grade);
    session.save(course);
    session.save(course2);
    session.save(course3);
    //提交事务
    transaction.commit();
    //释放资源
    session.close();
}

2. 执行该测试方法,控制台输出如下。
Hibernate:
   
    alter table student
       add constraint FKrahcsicxucn7bhee8empkb42c
       foreign key (gid)
       references grade (id)
Hibernate:
   
    alter table student_course
       add constraint FKkx4bkddvbfs0ese9v7hc5rycg
       foreign key (cid)
       references course (id)
Hibernate:
   
    alter table student_course
       add constraint FK8era63dfxi3csvjpresf6fdgu
       foreign key (sid)
       references student (id)

Hibernate:
    insert
    into
        student
        (name, gid)
    values
        (?, ?)

Hibernate:
    insert
    into
        student
        (name, gid)
    values
        (?, ?)

Hibernate:
    insert
    into
        grade
        (name)
    values
        (?)

Hibernate:
    insert
    into
        course
        (name)
    values
        (?)

Hibernate:
    insert
    into
        course
        (name)
    values
        (?)

Hibernate:
    insert
    into
        course
        (name)
    values
        (?)

Hibernate:
    update
        student
    set
        name=?,
        gid=?
    where
        id=?

Hibernate:
    update
        student
    set
        name=?,
        gid=?
    where
        id=?

Hibernate:
    update
        student
    set
        gid=?
    where
        id=?

Hibernate:
    update
        student
    set
        gid=?
    where
        id=?

Hibernate:
    insert
    into
        student_course
        (cid, sid)
    values
        (?, ?)

Hibernate:
    insert
    into
        student_course
        (cid, sid)
    values
        (?, ?)

Hibernate:
    insert
    into
        student_course
        (cid, sid)
    values
        (?, ?)

Hibernate:
    insert
    into
        student_course
        (cid, sid)
    values
        (?, ?)

从控制台输出可知,除了建表和 insert 语句外,Hibernate 还执行了 update 语句,而这些 update 语句正是由于使用双向关联而产生的多余 SQL 语句,它们会导致程序的运行效率降低。

3. 到数据库中,分别查询学生表(stduent)、班级表(grade)、课程表(course)和中间表(student_course)中的数据,结果如下。

图4:多对多-查询结果

反转

在前面介绍“一对多”还是“多对多”关联关系时,我们都采用是双向关联,即关联的双方都对关联关系进行了维护,例如在学生和班级这种一对多关联中,既描述了学生与班级的关系,又描述了班级与学生的关系。

这种双向关联的方式可以让 Hibernate 同时控制双方的关系,但在程序运行时,却很容易产生多余的 SQL 语句,造成重复操作的问题。此时,我们可以使用 Hibernate 提供的反转(inverse)功能,来解决此问题。

在映射文件的 <set> 标签中,有一个 inverse(反转)属性,它的作用是控制关联的双方由哪一方管理关联关系。

inverse 属性的取值是 boolean 类型的,当 inverse 属性取值为 false(默认值)时,表示由当前这一方管理双方的关联关系,如果双方 inverse 属性都为 false,双方将同时管理关联关系;取值为 true 时,表示当前一方放弃控制权,由对方管理双方的关联关系。

在一对多关联关系中,通常我们会将“一”的一方的 inverse 属性取值为 true,即由“多”的一方来维护关联关系;而在多对多关联关系中,则任意设置一方的 inverse 属性为 true 即可。

由于学生和班级之间的关联关系是一对多,而学生和课程之间的关联关系是多对多,因此我们可以将这两种关联关系的控制权都交给学生(Student)。下面我们就通过一个实例来演示反转功能。

示例

1. 修改映射文件 Grade.hbm.xml 的配置,在其 <set> 标签中,使用 inverse(反转)属性,并将该属性的值设置为 true,使其丧失对关联关系的控制权,由 Student 来管理它们之间的关联关系,配置如下。
<?xml version='1.0' encoding='utf-8'?>
<!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="net.biancheng.www.po.Grade" table="grade" schema="bianchengbang_jdbc">
        <id name="id" column="id" type="java.lang.Integer">
            <generator class="native"></generator>
        </id>
        <property name="name" column="name" length="100" type="java.lang.String"/>
        <!--设置 inverse 属性为 true,使其丧失对关联关系的控制权,由 Student 来管理关联关系-->
        <set name="students" inverse="true">
            <key column="gid"></key>
            <one-to-many class="net.biancheng.www.po.Student"></one-to-many>
        </set>
    </class>
</hibernate-mapping>

2. 修改映射文件 Course.hbm.xml 的配置,在其 <set> 标签中,使用 inverse(反转)属性,并将该属性的值设置为 true,使其丧失对关联关系的控制权,由 Student 来管理它们之间的关联关系,配置如下。
<?xml version='1.0' encoding='utf-8'?>
<!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="net.biancheng.www.po.Course" table="course" schema="bianchengbang_jdbc">
        <id name="id" column="id">
            <generator class="native"></generator>
        </id>
        <property name="name" column="name" length="100"/>
        <!--设置 inverse 属性为 true,使其丧失对关联关系的控制权,由 Student 来管理关联关系-->
        <set name="students" table="student_course" inverse="true">
            <key column="cid"></key>
            <many-to-many class="net.biancheng.www.po.Student" column="sid"></many-to-many>
        </set>
    </class>
</hibernate-mapping>

3. 由于所有的关联关系都已经交给了 Student 进行管理,因此我们在测试类中,只能通过 Student 对关联关系进行维护。修改测试类 MyTest 的 testManyToManySave() 方法,代码如下。
/**
* 多对多
* 保存操作
*/
@Test
public void testManyToManySave() {
    Session session = HibernateUtils.openSession();
    Transaction transaction = session.getTransaction();
    transaction.begin();
   
    //新建一个班级信
    Grade grade = new Grade();
    grade.setName("三年级(反转)");
   
    //新建两个学生
    Student student = new Student();
    student.setName("选课学生1(反转)");
    Student student2 = new Student();
    student2.setName("选课学生2(反转)");
   
    //新建三个课程
    Course course = new Course();
    course.setName("Java(反转)");
    Course course2 = new Course();
    course2.setName("PHP(反转)");
    Course course3 = new Course();
    course3.setName("C++(反转)");

    //由于所有的关联关系控制权都交给了Student,因此只能由Stduent对关联关系进行维护
    student.setGrade(grade);
    student.getCourses().add(course);
    student.getCourses().add(course2);
    student.getCourses().add(course3);
    student2.setGrade(grade);
    student2.getCourses().add(course);
    student2.getCourses().add(course3);

    //保存数据
    session.save(course);
    session.save(course2);
    session.save(course3);
    session.save(grade);
    session.save(student);
    session.save(student2);

    //提交事务
    transaction.commit();
    session.close();
}

执行该测试方法,控制台输出如下。
Hibernate:
   
    alter table student
       add constraint FKrahcsicxucn7bhee8empkb42c
       foreign key (gid)
       references grade (id)
Hibernate:
   
    alter table student_course
       add constraint FKkx4bkddvbfs0ese9v7hc5rycg
       foreign key (cid)
       references course (id)
Hibernate:
   
    alter table student_course
       add constraint FK8era63dfxi3csvjpresf6fdgu
       foreign key (sid)
       references student (id)

Hibernate:
    insert
    into
        course
        (name)
    values
        (?)

Hibernate:
    insert
    into
        course
        (name)
    values
        (?)

Hibernate:
    insert
    into
        course
        (name)
    values
        (?)

Hibernate:
    insert
    into
        grade
        (name)
    values
        (?)

Hibernate:
    insert
    into
        student
        (name, gid)
    values
        (?, ?)

Hibernate:
    insert
    into
        student
        (name, gid)
    values
        (?, ?)

Hibernate:
    insert
    into
        student_course
        (sid, cid)
    values
        (?, ?)

Hibernate:
    insert
    into
        student_course
        (sid, cid)
    values
        (?, ?)

Hibernate:
    insert
    into
        student_course
        (sid, cid)
    values
        (?, ?)

Hibernate:
    insert
    into
        student_course
        (sid, cid)
    values
        (?, ?)

Hibernate:
    insert
    into
        student_course
        (sid, cid)
    values
        (?, ?)

从控制台输出可以看出,使用了 inverse(反转)功能后,并没有产生多余的 update 语句。

级联

Hibernate 还提供了级联功能,当拥有关联关系控制权的一方,对数据库进行操作时,为了使数据保持同步,其关联对象也执行同样的操作。

在 Hibernate 的映射文件中有一个 cascade 属性,该属性用于设置关联对象是否进行级联操作,其常用属性值,如下表。

属性 描述
save-update 在进行保存或更新时,进行级联操作。
delete 在进行删除操作时,进行级联操作。
delete-orphan 孤儿删除,删除和当前对象解除关联关系的对象。
该属性值仅在关联关系为一对多时有效,因此只有此时才会存在父子关系,其中“一”的一方为“父方”,“多”的一方为“子方”;
例如,班级和学生是一对多的关系,其中班级为“父方”,学生为“子方”,当学生与班级解除了关联关系后,其外键被重置为了 null,那么这个学生也就失去了他的学生身份,这种记录就需要从学生表中删除。
all 所有情况下均进行级联操作, delete-orphan (孤儿删除)除外。
all-delete-orphan 所有情况下均进行级联操作, 包括 delete-orphan (孤儿删除)。
none 默认值,表示所有情况下,均不进行级联操作。

下面我们以“班级-学生-课程”为例,演示级联保存、级联删除以及孤儿删除等。

级联保存

班级和学生的关联关系为一对多,而学生与课程的关联关系则为多对多,基于这些关联关系,我们可以在保存班级信息时,级联保存学生和课程信息。

1. 修改映射文件 Grade.hbm.xml,在 <set> 标签中,设置 cascade 属性的值为 save-update,以达到保存班级信息时,级联保存学生信息的目的,具体配置如下。
<?xml version='1.0' encoding='utf-8'?>
<!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="net.biancheng.www.po.Grade" table="grade" schema="bianchengbang_jdbc">
        <id name="id" column="id" type="java.lang.Integer">
            <generator class="native"></generator>
        </id>
        <property name="name" column="name" length="100" type="java.lang.String"/>
        <!--设置 cascade 属性为 save-update,在保存班级信息时,级联保存学生信息-->
        <set name="students" inverse="false" cascade="save-update">
            <key column="gid"></key>
            <one-to-many class="net.biancheng.www.po.Student"></one-to-many>
        </set>
    </class>
</hibernate-mapping>
注意:使用 cascade 属性进行级联操作时,其必须具备管理或控制关联关系的能力,即其 inverse 属性必须取值为 false(默认值)。
2. 修改映射文件 Student.hbm.xml,在 <set> 标签中,设置 cascade 属性的值为 save-update,以达到保存学生信息时,级联保存选课信息的目的,具体配置如下。
<?xml version='1.0' encoding='utf-8'?>
<!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="net.biancheng.www.po.Student" table="student" schema="bianchengbang_jdbc" lazy="true">
        <id name="id" column="id" type="java.lang.Integer">
            <generator class="native"></generator>
        </id>
        <property name="name" column="name" length="100" type="java.lang.String"></property>

        <many-to-one name="grade" class="net.biancheng.www.po.Grade" column="gid" />
        <!--设置 cascade 属性为 save-update,通过 student 进行级联保存-->
        <set name="courses" table="student_course" cascade="save-update">
            <key column="sid"></key>
            <many-to-many class="net.biancheng.www.po.Course" column="cid"></many-to-many>
        </set>
    </class>
</hibernate-mapping>

3. 在测试类 MyTest 中,添加一个 testCascadeSave() 用来测试级联保存,代码如下。
@Test
public void testCascadeSave() {
    Session session = HibernateUtils.openSession();
    Transaction transaction = session.getTransaction();
    //开启事务
    transaction.begin();
    //创建班级
    Grade grade = new Grade();
    grade.setName("三年级");
    Grade grade2 = new Grade();
    grade2.setName("四年级");

    //新建三个课程
    Course course = new Course();
    course.setName("Java 基础");
    Course course2 = new Course();
    course2.setName("Java 面向对象 ");
    Course course3 = new Course();
    course3.setName("Java 高级特性");

    //创建是三个学生
    Student student = new Student();
    student.setName("张三");
    //学生选课
    student.getCourses().add(course);
    student.getCourses().add(course2);
    student.getCourses().add(course3);

    Student student2 = new Student();
    student2.setName("李四");
    //学生选课
    student2.getCourses().add(course);
    student2.getCourses().add(course2);

    Student student3 = new Student();
    student3.setName("赵六");
    //学生选课
    student3.getCourses().add(course2);
    student3.getCourses().add(course3);

    //将学生分配到班级中
    grade.getStudents().add(student);
    grade.getStudents().add(student2);
    grade2.getStudents().add(student3);

    //只保存班级信息,学生和选课信息则通过级联保存
    session.save(grade);
    session.save(grade2);
    //提交事务
    transaction.commit();
    //释放资源
    session.close();
}

4. 为了防止其他数据干扰,我们可以在核心配置文件 hibernate.cfg.xml 中,设置 hibernate.hbm2ddl.auto 属性的值为 create,使数据库表在每次运行时,会删除并重新创建,保证表中只有本次运行的数据(选配)。
<property name="hibernate.hbm2ddl.auto">create</property>

5. 运行测试方法,执行成功后到数据库中,分别对 student 表、grade 表、course 表和中间表 student_course 进行查询,结果如下图。

图5:级联保存结果

从以上查询结果可知,虽然我们只保存班级(grade)信息,但 Hibernate 仍然通过级联操作,将学生信息、课程信息以及选课信息(中间表)都保存到了数据库中。

级联删除

基于班级和学生的“一对多”关联关系,我们可以在删除班级信息时,级联删除班级内的学生信息。

1. 修改映射文件 Grade.hbm.xml,并在<set> 标签中的  cascade 属性新增属性值“delete”,配置如下。
<?xml version='1.0' encoding='utf-8'?>
<!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="net.biancheng.www.po.Grade" table="grade" schema="bianchengbang_jdbc">
        <id name="id" column="id" type="java.lang.Integer">
            <generator class="native"></generator>
        </id>
        <property name="name" column="name" length="100" type="java.lang.String"/>
        <!--设置 cascade 属性为 save-update,delete-->
        <set name="students" inverse="false" cascade="save-update,delete">
            <key column="gid"></key>
            <one-to-many class="net.biancheng.www.po.Student"></one-to-many>
        </set>
    </class>
</hibernate-mapping>
注意:cascade 属性可以包含多个值,中间用“,”隔开。
2. 在测试类 MyTest 中,添加一个名为 testCascadeDelete 测试方法,删除“四年级”的信息,代码如下。
@Test
public void testCascadeDelete() {
    Session session = HibernateUtils.openSession();
    Transaction transaction = session.getTransaction();
    //开启事务
    transaction.begin();
    // HQL 查询四年级信息
    Query query = session.createQuery("from Grade WHERE name=?1");
    query.setParameter(1, "四年级");
    List<Grade> resultList = query.getResultList();
    for (Grade grade : resultList) {
        //删除四年级的信息
        session.delete(grade);
    }
    //提交事务
    transaction.commit();
    //释放资源
    session.close();
}

3. 执行该测试方法,然后到数据库中分别对 student 表、grade 表、course 表和中间表 student_course 进行查询,结果如下图。
 

级联删除
图6:级联删除结果


从查询结果可以看出,我们在删除班级信息时,还同时级联删除了相关的 student(学生)和中间表(student_course)的信息。

Student.hbm.xml 在维护 Student 和 Course 关联映射时,并没有通过 cascade 属性开启级联删除功能,因此课程信息没有被级联删除。

级联操作是具有方向性的,在决定级联删除的方向时,需要以现实需求为标准,否则会出现不符合常理的错误,例如,级联删除的方向是从 student 到 grade 的话,则就会出现因为一个学生而删掉整个班级的尴尬局面。

孤儿删除

班级和学生的关联关系是“一对多”,此时它们是存在父子关系的,其中班级为“父方”,学生为“子方”。如果学生表中某条数据的外键(gid)就会被重置为 null,即该学生与班级解除了关联关系,不再属于任何一个班级,那么这个学生也就没有了存在的意义,这种数据就被称为孤儿数据,孤儿删除就是用来删除这种“孤儿数据”的。

1. 修改映射文件 Grade.hbm.xml,设置 cascade 属性的取值为 delete-orphan,配置如下。
<?xml version='1.0' encoding='utf-8'?>
<!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="net.biancheng.www.po.Grade" table="grade" schema="bianchengbang_jdbc">
        <id name="id" column="id" type="java.lang.Integer">
            <generator class="native"></generator>
        </id>
        <property name="name" column="name" length="100" type="java.lang.String"/>
        <!--设置 cascade 属性为 delete-orphan(孤儿删除)-->
        <set name="students" inverse="false" cascade="save-update,delete-orphan">
            <key column="gid"></key>
            <one-to-many class="net.biancheng.www.po.Student"></one-to-many>
        </set>
    </class>
</hibernate-mapping>

2. 在测试类 MyTest 中,添加一个名为 testDeleteOrphan 的测试方法,代码如下。
@Test
public void testDeleteOrphan() {
    Session session = HibernateUtils.openSession();
    Transaction transaction = session.getTransaction();
    //开启事务
    transaction.begin();
    // HQL 查询三年级信息
    Query query = session.createQuery("from Grade WHERE name=?1");
    query.setParameter(1, "三年级");
    List<Grade> gradeList = query.getResultList();
    //遍历结果集
    for (Grade grade : gradeList) {
        //HQL 查询名为张三的学生信息
        Query query1 = session.createQuery("from Student where name=?1");
        query1.setParameter(1, "张三");
        List<Student> studentList = query1.getResultList();
        //将学生张三与班级解除关系,使之称为孤儿
        grade.getStudents().removeAll(studentList);
    }
    //提交事务
    transaction.commit();
    //释放资源
    session.close();
}

3. 执行该测试方法,然后到数据库中分别对 student 表、grade 表、course 表和中间表 student_course 进行查询,结果如下图。


图7:孤儿删除结果

从图 3 可以看到,由于学生表中的“张三”与班级表解除了关联关系(外键重置为 null),当我们在班级的映射文件中设置了孤儿删除后,该学生信息及其相关表中数据就被自动删除了。  
注意,在介绍级联保存时,我们为了防止其他数据的干扰,而将核心配置文件 hibernate.cfg.xml 中 hibernate.hbm2ddl.auto 属性设置为了 create,这会导致每次执行程序时,都重新创建数据库表。在执行级联删除或孤儿删除时,需要我们重新将 hibernate.hbm2ddl.auto 属性设置为 update。