上篇文章我们对持久化对象进行的学习,了解了它的三种不同的状态并通过它完成对数据库的映射操作。但这都是基于单张表的操作,如果两张或者两张以上的表之间存在某种关联,我们又该如何利用持久化对象进行操作呢?本篇主要介绍的关联映射就是针对有着某种关联的多张表的各种操作,主要涉及内容如下:
- 组合主键的映射
- 组件的映射
- 单向多对一的映射
- 单向一对多的映射
- 双向一对多的映射
- 级联映射
一、组合主键的映射操作
根据我们的上篇文章,对于单一主键,在对象映射配置文件中使用 id标签即可完成配置。但是,往往有些主键并不是单一的,它可能由多个字段组合,那么此时就不能使用 id标签进行指定了。例如,我们有一张scores表,该表有三个字段,uid表示学生id,sub表示学生考试的学科,score表示该门考试成绩。那么确定一个学生的某门考试成绩就需要uid和usub,此时它们就是scores表的主键,因为它能唯一确定一行数据。
/*定义scores实体类*/
public class Scores {
private Score scoreId; //主键
private int score;
//省略get,set方法
}
public class Score implements Serializable {
private int userId;
private String sub;
//省略get,set方法
}
在这里,我们定义scores实体类,我们将主键封装成一个类score,该类必须继承接口Serializable ,最好还能实现它的两个方法equals和hashcode。然后就是我们的实体映射配置文件的编写:
<class name="DbClasses.Scores" table="scores">
<composite-id name="scoreId" class="DbClasses.Score">
<key-property name="userId" column="userId"></key-property>
<key-property name="sub" column="sub"></key-property>
</composite-id>
<property name="score" column="score"></property>
</class>
对于组合主键,我们使用标签composite-id来配置,name和class属性分别指定主键类在实体类中的名称及其位置。该标签下的key-property标签则是用来指定主键成员对应于数据表中的具体字段的。我们运行程序,看看Hibernate为我们创建的表中是否有一个组合主键:
显然,在我们的scores表中,userId和sub的组合构成了该表的主键。这就是组合主键在Hibernate中的配置情况,组合主键还是比较常见的。
二、组件映射
这里将要介绍的组件映射和上述介绍的主键映射名称相似,但确实完全不同的概念,需要予以区别。假设我们有一张person表:
person表中有主键id,name,age字段,还有三个地址字段(往往一个人有多个地址,我们要分别进行保存)。但是这样的一个表结构对应于我们的实体类如下:
public class Person {
private int id;
private String name;
private int age;
private String address1;
private String address2;
private String address3;
//省略get,set方法
}
对于这样的实体类来说,我们觉得他对于地址字段的处理是冗余的,假如某个人有十个地址,难道要在我们的实体类中配置十个属性吗?显然是不合理的,Hibernate允许我们像主键映射一样将所有的地址字段抽象出来一个类。
public class Person {
private int id;
private String name;
private int age;
private Address address;
//省略get,set方法
}
public class Address {
private String address1;
private String address2;
private String address3;
//省略get,set方法
}
重点在于我们的实体映射文件的配置,
<class name="DbClasses.Person" table="person">
<id name="id" column="id">
<generator class="native"></generator>
</id>
<property name="name" column="name"></property>
<property name="age" column="age"></property>
<!--映射的一个组件属性-->
<component name="address" class="DbClasses.Address">
<property name="address1" column="address1"></property>
<property name="address2" column="address2"></property>
<property name="address3" column="address3"></property>
</component>
</class>
实体类中其他的属性照常配置,对于这个Address类型的属性,我们使用component标签进行配置,name和class分别指定组件名和其位置,在该标签下,使用property标签配置组件的成员对应于数据表中的字段。然后我们删除表,重新看看这次Hibernate为我们生成的表结构:
显然结果是一样的,我们使用组件映射的一个好处就在于在这个实体类中,对于数据表结构显得非常清晰,代码的封装性更好,方便查错。
三、单向多对一的映射
以上介绍的两种基本映射并不属于我们本篇将要介绍的关联映射,关联映射就是指在处理多张有关联的表时,我们的实体类的配置。所谓的多对一就是指,其中一张表的主键是另一张表的外键,例如:
我们有一张Student表,一张grade表,其中grade表的主键id是Student表的外键(grade),Student中的多条记录对应于grade的一条记录,所以这种表的关联又被称作多对一的关联关系。下面我们看看如何通过对实体类的配置达到构建这种多对一的数据表关联。
public class Student {
private int uId;
private String name;
private int age;
private Grade grade;
//省略get,set方法
}
public class Grade {
private int id;
private String grade;
//省略get,set方法
}
Student和Grade分别对应不同数据库表的两个实体类,但是Student实体类中有一个属性grade指向实体类Grade。实体类映射配置文件如下:
<class name="DbClasses2.Student" table="student">
<id name="uId" column="uid">
<generator class="native"></generator>
</id>
<property name="name"></property>
<property name="age"></property>
<many-to-one name="grade" class="DbClasses2.Grade" column="grade_id"></many-to-one>
</class>
<class name="DbClasses2.Grade" table="grade">
<id name="id" column="id">
<generator class="native"></generator>
</id>
<property name="grade" column="grade"></property>
</class>
Grade实体类的配置没什么变化,Student中使用many-to-one标签将本实体类中属性grade配置指向另一个实体类Grade,并用column指定外键名称。也就是当Hibernate根据映射配置文件创建数据表的时候,发现属性grade指向的是一个实体类Grade,于是把Grade表的主键关联到grade字段上。我们先运行程序看看HIbernate是否为我们创建了这种外键关联,然后通过插入数据进一步理解Hibernate在底层为我们做的事情。
显然,在分别创建Student和Grade表之后,Hibernate又向数据库发送了一条alter语句,该语句负责添加外键关联。下面我们看看能否利用外键获取到Grade表中的成绩。
/*首先向表中插入信息*/
Student student = new Student();
student.setName("single");
student.setAge(21);
Grade grade = new Grade();
grade.setGrade("优秀");
student.setGrade(grade);
session.save(grade);
session.save(student);
我们知道,一个实体类的对象对应于数据表的一条记录,那么grade代表Grade表的一条记录,而该对象作为属性值被赋值给Student中的grade属性则表示它将自己的引用交给了Student的外键字段,也就是说student这条记录可以通过外键字段找到grade代表的这条记录。有点绕,但是学过数据库原理的应该不难理解。下面我们看,如何利用外键获取对应的Grade表中的一条完整记录。
Student student = (Student)session.get(Student.class,1);
Grade grade = student.getGrade();
System.out.println(grade.getId()+":"+grade.getGrade());
输出结果:
1:优秀
显然,我们通过Student返回的grade对象代表的就是基于Student外键字段值在Grade表中的一条数据。
四、单向一对多的映射
单向many-to-one关联是最常见的单向关联关系,其逻辑也趋近与我们的Sql语言,还算比较好理解。而对于单向一对多的映射则是其的一个逆向的逻辑,相对而言比较难以理解。这个多对一和一对多之间有个很明显的区别,对于多对一的情况,我们在得到Student对象代表的一条数据记录时,可以利用外键得到相对应Grade表中的一条记录。但是反过来,如果我们想知道对于Grade表的某条记录究竟有多少Student表记录予以对应呢?起码这是多对一无法直接解决的,那么我们的一对多则着重解决的就是这么一个问题。
所谓的一对多就是利用一的一方完成这种外键关联的构建。我们先看实体类的定义:
/*student实体类的定义*/
public class Student {
private int uId;
private String name;
private int age;
//省略get,set方法
}
public class Grade {
private int id;
private String grade;
//用于给其他表映射外键
private Set<Student> students = new HashSet<Student>(0);
//省略get,set方法
}
这里我们使用set集合,其实无论是list或者map都是可以的,旨在保存多的一方的记录。看似毫无关联的两张表却可以通过配置文件完成外键关联操作。有关Student实体的映射配置部分代码和平常是一样的,没有变动此处不再贴出,我们主要看Grade实体类的映射配置代码:
<class name="DbClasses2.Grade" table="grade">
<id name="id" column="id">
<generator class="native"></generator>
</id>
<property name="grade"/>
<set name="students">
<key column="grade_id"></key>
<one-to-many class="DbClasses2.Student"></one-to-many>
</set>
</class>
在Grade的实体类映射配置文件中,set标签用于配置属性students 。也就是说,当Hibernate加载到这里的时候,两张表单独创建完成之后,我要回到这里来,这里有一个一对多的外键需要更新,该外键的表载体在Student中,外键的名称是grade_id,于是它就会去更新Student的表结构,为它添加外键的引用,而引用的表就是Grade。这里还看不出set的作用,我们先看Hibernate为我们创建的表关联是否正确,然后通过存取数据来感受set的作用。
显然,Hibernate是先单独创建两张表,然后发送alter语句添加外键引用。那究竟set有什么用呢?它里面装的又是什么呢?
假设两张表中有如下信息:
下面我们通过程序获取成绩为优秀的所有Student。这一点在多对一映射中是做不到的。多对一只能知道某个学生的成绩是什么,但是无法直接知道成绩为什么的所有学生。
Grade grade = (Grade)session.get(Grade.class,1);
Set<Student> students = grade.getStudents();
//遍历输出所有学生姓名
Iterator<Student> iterator = students.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next().getName());
}
输出结果:
从Hibernate的日志输出中,我们可以很显然的看出来,首先Hibernate向数据库发送第一条select语句查询id为1的grade记录,然后默默的又一次发送select语句,不过这次是Student表,查询所有grade_id为1的记录并通过反射全部添加到set集合中。于是我们可以遍历输出所有Student信息。
五、双向一对多的映射
双向一对多或者双向多对一都是一个意思,这种形式的关联映射操作就是上述的两种映射的结合,在多的一段配置多对一映射,在一的一段配置一对多映射。这样,我们既可以从多的一端通过外键获取到一的一端的详细记录又可以从一的一端通过自己的主键获取到多的一端的所有对应记录。这种方式比较简单,此处不再赘述,我们最后看看两个关键字cascade和inverse的含义和用法。
六、级联映射
我们首先看级联操作,级联就是在两张具有关联关系的表操作的时候,通过操作其中一张表级联的更新了另一张表。先看个例子:
<many-to-one name="grade" class="DbClasses2.Grade" column="grade_id" cascade="save-update" />
我们在多的一端配置cascade等于save-update,意味着我们在多的一端进行save和update的时候,数据会自动更新到Grade表中。
Student stu1 = new Student();
stu1.setName("single");
stu1.setAge(21);
Grade grade = new Grade();
grade.setGrade("优秀");
stu1.setGrade(grade);
//session.save(grade);
session.save(stu1);
上述的这段代码完成的是一个插入操作,如果没有设置级联的话,该段程序必然报错,因为grade表中无任何数据,而student代表的一条记录的grade_id的字段却被强行插入数值1,自然会报错(外键1在grade表中找不到)。但是我们配置了级联就不一样了,Hibernate会先保存grade到数据库中,然后再插入student这条记录。从Hibernate的输出日志中也可以看出来:
当然,除了可以在多的一端配置级联,我们也可以在一的一端配置级联,让一的一端也可以级联的操作多的一端。例如:
<set name="students" cascade="save-update">
<key column="grade_id"></key>
<one-to-many class="DbClasses2.Student"></one-to-many>
</set>
给一的一端配置级联,然后我么通过级联来保存多的一端的数据。
Grade grade = new Grade();
grade.setGrade("优秀");
Student stu1 = new Student();
stu1.setName("single");
stu1.setAge(21);
Student stu2 = new Student();
stu2.setName("cyy");
stu2.setAge(20);
grade.getStudents().add(stu1);
grade.getStudents().add(stu2);
//session.save(stu1);sesion.save(stu2);
session.save(grade);
这段代码在没有配置级联的状态下,必然会报错。首先Hibernate根据配置文件创建了两张表及其之间的关联关系。执行save的时候会将grade保存到数据表中,然后Hibernate会查看自己set集合中对应的多端的记录并根据这些记录去更新多端表中的外键值,当然如果没有保存到student表中,自然会报错。我们看看级联是怎么做的:
显然,在保存好grade之后,立马将自己set集合中的Student记录插入到Student表中,然后通过update语句更新他们的外键值。而没有设置级联的话,第二三条Sql语句是没有的,报错那也是自然的。关于级联,只要理解了它的本质,这些操作也都是可以理解的,本质上就是在做插入或者修改操作的时候如果发现自己代表的这条记录中有外部关联表的内容,那么则先完成对外部表的更新。这就是级联,级联的操作和自己关联的外部表,当然cascade也不止这一个参数值:
cascade="all|none|save-update|delete"
其中,none表示不级联,all表示所有操作都级联,save-update 表示保存和修改操作进行级联,delete表示删除的时候级联删除。本质都类似,此处不再赘述。
至此,有关关联映射的第一部分介绍完了,下篇将继续介绍未完的其他关联映射的操作。总结不到之处,望指出!