第 1 章主要讲解了 Hibernate,它是一个开放源代码的对象关系映射框架。通过对JDBC 进行轻量级的对象封装,使 Java 程序员能够随心所欲地使用面向对象的编程思维来操作数据库。作为目前最杰出的 0-R Mapping 框架,Hibernate 的核心是能够支持对象间关系的良好映射。在面向对象设计与实体模型关系中,对象间关系一般包括 4 种:一对一 ( one-to-one)、一对多 ( one-to-many)、多对一(many-to-one)、多对多 (many-to-many)。本章将对一对多、多对一、多对多关联关系进行讲解,了解关联关系的含义以及关联操作的优势。
核心技能部分
1.1 映射关联关系
1.1.1 什么是关联关系
类与类之间最普遍的关系就是关联关系,例如老师和学生之间存在的对应关系,在UML中关联关系是有方向的,以我们的forum系统中的版块和帖子之间的关系为例,从版块这一端来看一个版块包含多个帖子,这是一对多关系,而从帖子这个角度看,多个帖子属于同一个版块这是多对一关系。关联关系在对象之间是通过持有对方引用的形式来体现,而在数据库中则体现为表与表之间的外键关联。
1.1.2 关联操作的优势
关联操作能够使存在关系的表之间保持数据的同步。同时,关联关系能够使程序员在编写程序的过程中减少对多表操作代码的编写,优化程序,提高程序的运行效率,例如在版块和帖子之间建立起合适的关联关系,我们就可以很方便的查询到版块所属的帖子。不必要的关联设计也可会导致系统性能底下,这是我们在系统开发中一定要注意的。
1.2 一对多、多对一关联
1.2.1 配置单向多对一关联
多对一关系是最常见、使用最广泛的一种关联关系,例如在我们的forum系统中帖子和版块、帖子和用户、回帖和帖子之间就是多对一关系。
简单的说,一个实体对象就是数据库表中的一行数据的对象化表示,在数据库中这种多对一关联关系可以通过外键加以描述,例如forum系统中的版块表和帖子表如图2.1.1所示。
这种关系如果使用对象来表示的话就是在Thread类中持有版块对象的引用 Thread类 Board 类代码如示例2.1 和示例2.2所示。
示例2.1
public class Thread implements Serializable {
private int id;
private int version;
private boolean deleted;
private Date dateCreated;
private String title;
private String content;
private String ipCreated;
//点击数
private int hit;
private Date dateLastReplied;
//是否只读
private boolean readonly;
//是否置顶
private boolean topped;
//回帖数量
private int replyCount;
//单向多对一
private Board board;
//…略getter setter
示例 2.2
public class Board implements Serializable {
private int id;
private int version;
private boolean deleted;
private Date dateCreated;
private String name;
private String description;
//帖子数量
private int threadCount;
//回复数量
private int replyCount;
//…略去getter setter
}
Board类的映射文件Board.hbm.xml非常简单,这里我们重点要看一看Thread的映射文件的关系属性如何映射!
Thread.hbm.xml代码如示例2.3所示。
示例2.3
<hibernate-mapping package=”com.yourcompany.forum.bean”>
<class name="Thread" table="Thread" catalog="forum">
<id name="id" type="java.lang.Integer">
<column name="id" />
<generator class="sequence">
<param name="sequence">SEQ_ID</param>
</generator>
</id>
<many-to-one name="board" class="Board" fetch="select">
<column name="board_id" />
</many-to-one>
<property name="dateCreated" type="java.sql.Timestamp">
<column name="dateCreated" length="0" />
</property>
<property name="deleted" type="java.lang.Boolean">
<column name="deleted" not-null="true" />
</property>
<property name="version" type="java.lang.Integer">
<column name="version" />
</property>
<property name="content" type="java.lang.String">
<column name="content" />
</property>
<property name="dateLastReplied" type="java.sql.Timestamp">
<column name="dateLastReplied" length="0" />
</property>
<property name="hit" type="java.lang.Integer">
<column name="hit" not-null="true" />
</property>
<property name="ipCreated" type="java.lang.String">
<column name="ipCreated" />
</property>
<property name="readonly" type="java.lang.Boolean">
<column name="readonly" not-null="true" />
</property>
<property name="replyCount" type="java.lang.Integer">
<column name="replyCount" not-null="true" />
</property>
<property name="title" type="java.lang.String">
<column name="title" />
</property>
<property name="topped" type="java.lang.Boolean">
<column name="topped" not-null="true" />
</property>
</class>
</hibernate-mapping>
其中<many-to-one name="board" class="Board" fetch="select"><column name="board_id" /></many-to-one>则指定在Thread表中添加一个外键指向Board表。最后不要忘记在hibernate.cfg.xml中引入以上两个映射文件。
下面我们编写一个测试类来测试结果,代码如示例2.4所示。
示例2.4
@Test
public void save() throws Exception {
Board board = new Board();
board.setName("莲蓬鬼话");
board.setReplyCount(0);
session.save(board);
Thread Thread1 = new Thread();
Thread1.setBoard(board);
Thread1.setTitle("鬼吹灯");
Thread Thread2 = new Thread();
Thread2.setBoard(board);
Thread2.setTitle("盗墓笔记");
session.save(Thread1);
session.save(Thread2);
}
运行后控制台输出如下sql语句,如图2.1.2所示。
我们发现只要在对象上设置了关联关系,Hibernate会自动完成到数据库的转换,在Hibernate中可以使用many-to-one标签来映射多对一关联,many-to-one常用属性如表2-1-1所示。
表2-1-1 many-to-one元素常用属性
属性 |
含义 |
必须 |
默认值 |
name |
属性的名称 |
Yes |
|
class |
关联类的类名 |
No |
|
column |
关联的字段 |
No |
|
not-null |
关了字段是否可以为空 |
No |
False |
lazy |
延迟加载 |
No |
Proxy |
fetch |
抓取策略 |
No |
select |
1.2.2 单向一对多关联
Thread对Board是多对一,反过来从Board来看则是一对多,两个实体类的代码如示例2.5 ,2.6所示。
示例2.5
public class Thread implements Serializable {
private int id;
private int version;
private boolean deleted;
private Date dateCreated;
private String title;
private String content;
private String ipCreated;
//点击数
private int hit;
private Date dateLastReplied;
//是否只读
private boolean readonly;
//是否置顶
private boolean topped;
//回帖数量
private int replyCount;
//…略getter setter
现在我们只考虑从版块到帖子的单向一对多关系,所以将Thread中的board引用去掉。
示例2.6
public class Board implements Serializable {
private int id;
private int version;
private boolean deleted;
private Date dateCreated;
private String name;
private String description;
//帖子数量
private int threadCount;
//回复数量
private int replyCount;
//单向一对多
private Set threads = new HashSet();
//…略去getter setter
}
一个Board中可以有多个Thread,所以可以使用一个Set来表示,但请注意 ,这里不要使用具体的集合类类型来声明这个关系属性(原因在后续课程会讲解)。
Board类的映射文件Board.hbm.xml内容如示例2.7所示。
示例2.7
<hibernate-mapping package=" com.yourcompany.forum.bean">
<class name="Board" table="board" catalog="forum">
<id name="id" type="java.lang.Integer">
<column name="id" />
<generator class="sequence">
<param name="sequence">SEQ_ID</param>
</generator>
</id>
<set name="threads">
<key column="board_id"></key>
<one-to-many class="Thread"/>
</set>
<property name="dateCreated" type="java.sql.Timestamp">
<column name="dateCreated" length="0" />
</property>
//…以下略去
</class>
</hibernate-mapping>
使用Set标签和one-to-many标签配置一对多关联,set元素的常用属性如表2-1-2所示。
表2-1-2 set元素的常用属性
属性 |
含义 |
必须 |
默认值 |
name |
集合属性的名称 |
Yes |
|
table |
关联类的目标数据库表名称 |
No |
|
cascade |
级联操作 |
No |
none |
key |
指定在对方表中加入外键列 |
Yes |
|
lazy |
延迟加载 |
No |
Proxy |
fetch |
抓取策略 |
No |
select |
下面我们编写一个测试类来测试结果,代码如示例2.8所示。
示例2.8
@Test
public void save() throws Exception {
Thread Thread1 = new Thread();
Thread1.setTitle("鬼吹灯");
Thread Thread2 = new Thread();
Thread2.setTitle("盗墓笔记");
session.save(Thread1);
session.save(Thread2);
Board board = new Board();
board.setName("莲蓬鬼话");
board.getThreads().add(Thread1);
board.getThreads().add(Thread2);
session.save(board);
}
运行后控制台输出如下sql语句,如图2.1.3所示。
观察数据库我们发现 其实单向一对多和单向多对一在数据库中是完全一样的。
到目前为止,无论是单向的多对一还是一对多关系中,我们都要逐一对实体类使用save方法将数据进行保存,如果要保存的数据非常多那么编码将会非常的麻烦,这时候我们可以使用一个非常实用的属性配置cascade! 修改Board.hbm.xml映射文件如图2.1.4所示。
修改测试类如示例2.9所示。
示例2.9
@Test
public void save() throws Exception {
Thread Thread1 = new Thread();
Thread1.setTitle("鬼吹灯");
Thread Thread2 = new Thread();
Thread2.setTitle("盗墓笔记");
//session.save(Thread1);
//session.save(Thread2);
Board board = new Board();
board.setName("莲蓬鬼话");
board.getThreads().add(Thread1);
board.getThreads().add(Thread2);
session.save(board);
}
在上例中,我们只是对Board对象执行了save操作,但Hibernate自动对所关联的对象执行了相同的操作,这就是cascade(级联)的含义。
Cascade属性的常用属性值如表2-1-3所示。
表2-1-3 Cascade属性的常用属性值
属性值 |
含义 |
delete |
对delete执行级联 |
save-update |
对save 和update操作执行级联 |
all |
所有操作都执行级联 |
none |
关闭级联 |
在两个对象映射了关联关系后,在开发中就可以在加载对象的同时,自动加载其关联的属性,减少了多表操作的复杂性,我们在上一章巩固练习部分给大家提出的问题,查询版块的同时,查询到其所属的所有帖子,如果使用关联的话,这是轻而易举的事情,代码如示例2.10所示。
示例2.10
@Test
public void find() throws Exception {
Board board = (Board)session.get(Board.class, 1);
System.out.println(board.getName());
Set<Thread> set = board.getThreads();
for(Thread Thread : set){
System.out.println(Thread.getTitle());
}
}
虽然单向一对多关联关系相对较简单,却存在一些问题。首先会出现多余的update语句严重影响系统的效率,而且数据库表结构的设计也受到限制,即外键列不能设置为非空,否则在Hibernate进行创建或更新时可能出现约束违例。而双向一对多就能解决这个问题。
1.2.3 双向一对多关联
单向多对一和单向一对多既可单独配置使用,也可以同时配置,如果双方同时配置了关系 ,就叫做双向一对多关联。
实体类代码如示例2.11, 2.12所示。
示例 2.11
public class Thread implements java.io.Serializable{
private int id;
//单向多对一
private Board board;
private int version;
private boolean deleted;
private Date dateCreated;
private String title;
private String content;
private String ipCreated;
//点击数
private int hit;
private Date dateLastReplied;
//是否只读
private boolean readonly;
//是否置顶
private boolean topped;
//回帖数量
private int replyCount;
//…略getter setter
示例 2.12
public class Board implements Serializable {
private int id;
private int version;
private boolean deleted;
private Date dateCreated;
private String name;
private String description;
//帖子数量
private int threadCount;
//回复数量
private int replyCount;
//单向一对多
private Set threads = new HashSet();
//…略去getter setter
}
映射文件如示例2.13所示。
示例2.13
<hibernate-mapping package="com.yourcompany.forum.bean">
<class name="Thread" table="Thread" catalog="forum">
<id name="id" type="java.lang.Integer">
<column name="id" />
<generator class="sequence">
<param name="sequence">SEQ_ID</param>
</generator>
</id>
<many-to-one name="board" class="Board">
<column name="board_id"></column>
</many-to-one>
//以下略去…
</class>
<class name="Board" table="board" catalog="forum">
<id name="id" type="java.lang.Integer">
<column name="id" />
<generator class="sequence">
<param name="sequence">SEQ_ID</param>
</generator>
</id>
<set name="threads" cascade="save-update">
<key column="board_id"></key>
<one-to-many class="Thread"/>
</set>
//以下略去…
</class>
测试代码如示例2.14所示。
示例2.14
@Test
public void save() throws Exception {
Thread Thread1 = new Thread();
Thread1.setTitle("鬼吹灯");
Thread Thread2 = new Thread();
Thread2.setTitle("盗墓笔记");
Board board = new Board();
board.setName("莲蓬鬼话");
board.getThreads().add(Thread1);
board.getThreads().add(Thread2);
Thread1.setBoard(board);
Thread2.setBoard(board);
session.save(board);
}
程序运行后我们发现,控制台仍然出现了update语句,单向一对多存在的问题并没有得到解决,现在我们对保存数据的过程加以分析:
1. 执行session.save(board);将board对象插入数据库,同时board拥有id值。
2. 对board中的所有Thread对象级联执行save操作,而Thread中又持有board的引用,所以在保存多端(Thread)的时候,外键列board_id已经被保存了。
3. 一端board 为了确保双方的关系,发出update语句更新board_id字段。
由上述分析可知 ,双方的关系字段在保存Thread的时候就已经保存了,board再更新关系字段(维护关系)完全没有必要,那有没有办法通知Board放弃对关系的维护呢?使用inverse属性就可以解决这个问题。
inverse可以直译为”反转”,在Hibernate中的含义为是否放弃维护关系。在关联关系中。inverse=’true’的一方将不负责关系的维护,称之为被控方,inverse=’false’的一方称之为主控方,负责关系的维护。Inverse的默认值为false。
一般在一对多关系中把one端设置为true,将有助于性能的改善。
现在我们修改映射文件如示例2.15所示。
示例2.15
<hibernate-mapping package="com.yourcompany.forum.bean">
<class name="Thread" table="Thread" catalog="forum">
<id name="id" type="java.lang.Integer">
<column name="id" />
<generator class="sequence">
<param name="sequence">SEQ_ID</param>
</generator>
</id>
<many-to-one name="board" class="Board">
<column name="board_id"></column>
</many-to-one>
//以下略去…
</class>
<class name="Board" table="board" catalog="forum">
<id name="id" type="java.lang.Integer">
<column name="id" />
<generator class="sequence">
<param name="sequence">SEQ_ID</param>
</generator>
</id>
<set name="threads" cascade="save-update" inverse=”true”>
<key column="board_id"></key>
<one-to-many class="Thread"/>
</set>
//以下略去…
</class>
经过了设置one方的inverse为true,在保存board的时候,不再主动update外键,而是在Thread端手动设置。
注意,实际开发中要根据实际情况来决定双向关系的使用,不要为了关联而关联,不必要的关联关系反倒会造成性能的浪费,特别是所关联的数据量很大的时候更是如此。
1.3 多对多关联
1.3.1 配置多对多关联
某校的管理系统中要解决如下问题:
每个学生可选报多门课程,每个课程有1-n个学生。
系统需要完成如下功能:
Ø 列出指定课程的所有选修的学生。
Ø 列出指定学生所选修的所有科目。
下面我们对这个问题进行分析。
学生和课程之间是典型的多对多关系,在对象模型上表现这种关系并不复杂,双方各设置一个集合即可,代码如示例2.16、2.17所示。
示例2.16
public class Student implements Serializable {
private Integer stu_id;
private String name;
private Set courses=new HashSet();
public Integer getStu_id() {
return stu_id;
}
public Set getCourses() {
return courses;
}
public void setCourses(Set courses) {
this.courses = courses;
}
public void setStu_id(Integer stuId) {
stu_id = stuId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Student() {
super();
// TODO Auto-generated constructor stub
}
}
示例2.17
public class Course implements Serializable {
private Integer course_id;
private String name;
private Set students=new HashSet();
public Set getStudents() {
return students;
}
public void setStudents(Set students) {
this.students = students;
}
public Integer getCourse_id() {
return course_id;
}
public void setCourse_id(Integer courseId) {
course_id = courseId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Course() {
super();
// TODO Auto-generated constructor stub
}
}
通过前面的学习,我们知道关系在数据库表中都表现为外键的引用,对上述问题,如何设计数据库?这时候需要第三张关联表才能体现其关系。
在映射文件中依然使用set元素进行配置,很显然这里需要指定要使用的第三张表,具体映射如示例2.18所示。
示例2.18
<class name="Course" table="tb_course">
<!--略去部分配置-- >
<set name="students" table="r_course_stu" inverse="true"> //table属性指定中间表 并将Course设置为被控方
<key column="r_course_id"></key>//在中间表添加外键关联到自己
<many-to-many class="Student" column="r_stu_id"></many-to-many>
//关联的Student 在中间表中插入外键r_stu_id;
</set>
</class>
<class name="Student" table="tb_student">
//略去部分
<set name="courses" table="r_course_stu" >
<key column="r_stu_id"></key>
<many-to-many class="Course" column="r_course_id"></many-to-many>
</set>
</class>
编写测试类如示例2.19所示。
示例2.19
Course course1= new Course();
Course course2= new Course();
course1.setName("java");
course2.setName(".net");
session.save(course1);
session.save(course2);
Student student1 = new Student();
Student student2 = new Student();
student1.setName("芙蓉");
student2.setName("犀利");
student1.getCourses().add(course1);
student1.getCourses().add(course2);
student2.getCourses().add(course1);
student2.getCourses().add(course2);
session.save(student1);
session.save(student2);
本章总结
什么是实体关联关系
关联操作
n 简化查询,提供开发效率
n 使用不当可能造成性问题
映射实体关联关系
n 单向一对多
n 单向多对一
n 双向一对多
多对多