前言:本文讲解使用Hibernate映射多对多关联关系,并使用多种方式映射多对多关联。
1.数据库表的多对多关系
本文根据学生信息表(tb_student)和教师信息表(tb_teacher)来说明多对多映射关系,一个学生有多个老师,一个老师也有多个学生,其数据库模型图如下:
根据以上图,对应的建表语句如下:
CREATE TABLE tb_student
(
id bigint NOT NULL auto_increment COMMENT 'ID',
no varchar(10) NOT NULL COMMENT '学号',
name varchar(50) NOT NULL COMMENT '姓名',
sex char(1) NOT NULL COMMENT '性别',
birthday datetime COMMENT '出生日期',
class_id bigint COMMENT '班级ID',
PRIMARY KEY (id)
) COMMENT = '学生信息表';
CREATE TABLE tb_teacher
(
id bigint NOT NULL auto_increment COMMENT 'ID',
no varchar(10) NOT NULL COMMENT '教师编号',
name varchar(50) NOT NULL COMMENT '教师姓名',
sex char(1) NOT NULL COMMENT '教师性别',
job_title varchar(50) NOT NULL COMMENT '职称',
PRIMARY KEY (id)
) COMMENT = '教师信息表';
CREATE TABLE tb_student_teacher
(
student_id bigint NOT NULL COMMENT '学生ID',
teacher_id bigint NOT NULL COMMENT '教师ID',
PRIMARY KEY (student_id,teacher_id)
) COMMENT = '学生教师关系表';
-- 可选的外键约束
ALTER TABLE tb_student_teacher ADD CONSTRAINT fk_student_id FOREIGN KEY (student_id) REFERENCES tb_student (id);
ALTER TABLE tb_student_teacher ADD CONSTRAINT fk_teacher_id FOREIGN KEY (teacher_id) REFERENCES tb_teacher (id);
编写对应的实体类代码如下:
package model;
import java.sql.Date;
import java.util.HashSet;
import java.util.Set;
public class Student
{
private Long id;
private String no;
private String name;
private String sex;
private Date birthday;
private Long classId;
private Set<Teacher> teachers=new HashSet<Teacher>();
@Override
public String toString()
{
return "Student [id=" + id + ", no=" + no + ", name=" + name + ", sex=" + sex + ", birthday=" + birthday + ", classId=" + classId + ", teachers=" + teachers.size() + "]";
}
//省略setter、getter...
}
package model;
import java.util.HashSet;
import java.util.Set;
public class Teacher
{
private Long id;
private String no;
private String name;
private String sex;
private String job_title;
private Set<Student> students=new HashSet<Student>();
@Override
public String toString()
{
return "Teacher [id=" + id + ", no=" + no + ", name=" + name + ", sex=" + sex + ", job_title=" + job_title + ", students=" + students.size() + "]";
}
//省略setter、getter...
}
2.单向多对多关系
多对多关系也有单向多对多和双向多对多,下面这个例子讲解从Teacher类到Student类的单向多对多关系,配置如下:
<hibernate-mapping package="model">
<class name="Student" table="tb_student">
<id name="id">
<generator class="native"></generator>
</id>
<property name="no" column="no"/>
<property name="name" column="name"/>
<property name="sex" column="sex"/>
<property name="birthday" column="birthday"/>
<property name="classId" column="class_id"/>
</class>
</hibernate-mapping>
<hibernate-mapping package="model">
<class name="Teacher" table="tb_teacher">
<id name="id" column="id">
<generator class="native"></generator>
</id>
<property name="no" column="no" />
<property name="name" column="name" />
<property name="sex" column="sex" />
<property name="job_title" column="job_title" />
<set name="students" table="tb_student_teacher" cascade="save-update">
<key column="teacher_id"/>
<many-to-many class="model.Student" column="student_id" />
</set>
</class>
</hibernate-mapping>
对于以上配置要注意一下几点:
- 由于实体类使用的是Set集合所以使用set元素配置关联关系,根据实体类的集合类型也可以对应的使用idbag、list、map元素。
- 由于tb_student表与tb_teacher表的多对多关系是通过tb_student_teacher表来保存的,所以set元素的table属性设置成table="tb_student_teacher"。
- set元素的key子元素设置column="teacher_id",表示tb_student_teacher表中参照tb_teacher表的外键设置为teacher_id。
- set元素的many-to-many子元素设置class="model.Student",表示Teacher类的students属性的类型,而其column="student_id",表示tb_student_teacher表中参照model.Student类型对应的表的外键设置为student_id。
- 所以set元素的cascade属性设置成save-update是很合理的,不允许把cascade设置成all、delete、all-delete-orphands,假如删除一个Teacher对象时还会级联删除与他关联的所有Student对象,由于Student对象可能还会与其他的Teacher对象关联,因此当Hibernate执行级联删除时会违反数据库的外键参照完整性!
测试程序如下:
public static void main(String[] args)
{
Teacher teacher;
Configuration cfg = new Configuration();
cfg.configure();
ServiceRegistry sr = new ServiceRegistryBuilder().applySettings(cfg.getProperties()).buildServiceRegistry();
SessionFactory sf = cfg.buildSessionFactory(sr);
System.out.println("连接数据库");
Session session = sf.openSession();
teacher =(Teacher) session.get(Teacher.class, new Long(1));
System.out.println(teacher);
System.out.println(teacher.getStudents());
session.close();
System.exit(0);
}
控制台输出如下:
连接数据库
Hibernate:
select
teacher0_.id as id1_7_0_,
teacher0_.no as no2_7_0_,
teacher0_.name as name3_7_0_,
teacher0_.sex as sex4_7_0_,
teacher0_.job_title as job_titl5_7_0_
from
tb_teacher teacher0_
where
teacher0_.id=?
Hibernate:
select
students0_.teacher_id as teacher_1_7_0_,
students0_.student_id as student_2_6_0_,
student1_.id as id1_5_1_,
student1_.no as no2_5_1_,
student1_.name as name3_5_1_,
student1_.sex as sex4_5_1_,
student1_.birthday as birthday5_5_1_,
student1_.class_id as class_id6_5_1_
from
tb_student_teacher students0_
inner join
tb_student student1_
on students0_.student_id=student1_.id
where
students0_.teacher_id=?
Teacher [id=1, no=000001, name=教师1, sex=女, job_title=初级教师, students=1]
Hibernate:
select
teachers0_.student_id as student_2_5_0_,
teachers0_.teacher_id as teacher_1_6_0_,
teacher1_.id as id1_7_1_,
teacher1_.no as no2_7_1_,
teacher1_.name as name3_7_1_,
teacher1_.sex as sex4_7_1_,
teacher1_.job_title as job_titl5_7_1_
from
tb_student_teacher teachers0_
inner join
tb_teacher teacher1_
on teachers0_.teacher_id=teacher1_.id
where
teachers0_.student_id=?
[Student [id=1, no=000001, name=学生1, sex=男, birthday=2015-01-27, classId=41, teachers=1]]
关闭数据库
根据输出可以看出Hibernate会根据inner join连接查询获取关联数据。
3.双向多对多关系
使用双向关联与单向关联类似,只是在Student类与Teacher类两方都配置了set元素进行关联,配置如下:
<hibernate-mapping package="model">
<class name="Student" table="tb_student">
<id name="id">
<generator class="native"></generator>
</id>
<property name="no" column="no"/>
<property name="name" column="name"/>
<property name="sex" column="sex"/>
<property name="birthday" column="birthday"/>
<property name="classId" column="class_id"/>
<set name="teachers" table="tb_student_teacher" cascade="save-update" inverse="true">
<key column="student_id"/>
<many-to-many class="model.Teacher" column="teacher_id"/>
</set>
</class>
</hibernate-mapping>
<hibernate-mapping package="model">
<class name="Teacher" table="tb_teacher">
<id name="id" column="id">
<generator class="native"></generator>
</id>
<property name="no" column="no" />
<property name="name" column="name" />
<property name="sex" column="sex" />
<property name="job_title" column="job_title" />
<set name="students" table="tb_student_teacher" cascade="save-update" inverse="false">
<key column="teacher_id"/>
<many-to-many class="model.Student" column="student_id" />
</set>
</class>
</hibernate-mapping>
注意:对于双向多对多关联的两端,必须把其中一端的<set>元素的inverse属性设置成true,同时建立关联时必须同时建立从Teacher类到Student类,以及从Student类到Teacher类的关联关系!
以上的关联两端都使用<set>元素,但是在双向多对多关联中值得注意的是:必须把其中的一端的inverse属性设置成true,另一端设置成false。并且在inverse="false"的一端可以使用set、idbag、list、map元素,而在inverse="true"的一端只能使用set元素和bag元素!由于Student是inverse="true"的一端,Hibernate不会根据teachers对象的集合的变化同步更新tb_student_teacher连接表,因此无法通过该连接表来保存Student对象的teachers集合中各个Teacher对象的索引位置。
4.使用组件类集合映射多对多关联
在上面的例子中Student类中有一个teachers集合属性,同样在Teacher类中有一个students集合属性,关联关系由Student类和Teacher类来维护。除了这种方式以外Hibernate还支持使用组件类的集合属性来维护关联关系,下面使用组件类集合映射多对多关系,数据库表结构不变,对实体类修改如下图:
对应的代码如下:
package model;
import java.sql.Date;
import java.util.HashSet;
import java.util.Set;
public class Student
{
private Long id;
private String no;
private String name;
private String sex;
private Date birthday;
private Long classId;
private Set<TeacherAndStudent> teacherAndStudents = new HashSet<TeacherAndStudent>();
@Override
public String toString()
{
return "Student [id=" + id + ", no=" + no + ", name=" + name + ", sex=" + sex + ", birthday=" + birthday + ", classId=" + classId + "]";
}
// 省略setter、getter...
}
package model;
import java.util.HashSet;
import java.util.Set;
public class Teacher
{
private Long id;
private String no;
private String name;
private String sex;
private String job_title;
private Set<TeacherAndStudent> teacherAndStudents = new HashSet<TeacherAndStudent>();
@Override
public String toString()
{
return "Teacher [id=" + id + ", no=" + no + ", name=" + name + ", sex=" + sex + ", job_title=" + job_title + "]";
}
// 省略setter、getter...
}
package model;
public class TeacherAndStudent
{
private Teacher teacher;
private Student student;
@Override
public String toString()
{
return "TeacherAndStudent [teacher=" + teacher + ", student=" + student + "]";
}
// 省略setter、getter...
}
其中组件类是TeacherAndStudent,相对于实体类他有一个特点就是没有OID!对应的配置映射文件如下:
<hibernate-mapping package="model">
<class name="Student" table="tb_student">
<id name="id">
<generator class="native"></generator>
</id>
<property name="no" column="no"/>
<property name="name" column="name"/>
<property name="sex" column="sex"/>
<property name="birthday" column="birthday"/>
<property name="classId" column="class_id"/>
<set name="teacherAndStudents" table="tb_student_teacher" cascade="save-update" inverse="true">
<key column="student_id" />
<composite-element class="TeacherAndStudent">
<parent name="student" />
<many-to-one name="teacher" class="Teacher" column="teacher_id"/>
</composite-element>
</set>
</class>
</hibernate-mapping>
<hibernate-mapping package="model">
<class name="Teacher" table="tb_teacher">
<id name="id" column="id">
<generator class="native"></generator>
</id>
<property name="no" column="no" />
<property name="name" column="name" />
<property name="sex" column="sex" />
<property name="job_title" column="job_title" />
<set name="teacherAndStudents" table="tb_student_teacher" cascade="save-update" inverse="false">
<key column="teacher_id"/>
<composite-element class="TeacherAndStudent">
<parent name="teacher"/>
<many-to-one name="student" class="Student" column="student_id"/>
</composite-element>
</set>
</class>
</hibernate-mapping>
对于以上的配置的说明如下:
- 在Hibernate中使用composite-element元素映射组件类。
- composite-element元素下的parent元素用来设置当前组件所属的整体类。
- composite-element元素下还可以使用many-to-one元素和property元素等。
关于组件映射的内容细节将会在后面的文章中讲到,在这里就不做深入的讲解了,测试代码如下:
public static void main(String[] args)
{
Teacher teacher;
Configuration cfg = new Configuration();
cfg.configure();
ServiceRegistry sr = new ServiceRegistryBuilder().applySettings(cfg.getProperties()).buildServiceRegistry();
SessionFactory sf = cfg.buildSessionFactory(sr);
System.out.println("连接数据库");
Session session = sf.openSession();
teacher =(Teacher) session.get(Teacher.class, new Long(1));
System.out.println(teacher);
System.out.println(teacher.getTeacherAndStudents());
session.close();
System.exit(0);
}
值得注意的是:由于TeacherAndStudent类是一个组件类(没有OID),所以不能调用Session的保存、更新、删除等方法对他进行操作!
一下是程序运行的结果:
连接数据库
Hibernate:
select
teacher0_.id as id1_7_0_,
teacher0_.no as no2_7_0_,
teacher0_.name as name3_7_0_,
teacher0_.sex as sex4_7_0_,
teacher0_.job_title as job_titl5_7_0_
from
tb_teacher teacher0_
where
teacher0_.id=?
Teacher [id=1, no=000001, name=教师1, sex=女, job_title=初级教师]
Hibernate:
select
teacherand0_.teacher_id as teacher_1_7_0_,
teacherand0_.student_id as student_2_6_0_,
student1_.id as id1_5_1_,
student1_.no as no2_5_1_,
student1_.name as name3_5_1_,
student1_.sex as sex4_5_1_,
student1_.birthday as birthday5_5_1_,
student1_.class_id as class_id6_5_1_
from
tb_student_teacher teacherand0_
left outer join
tb_student student1_
on teacherand0_.student_id=student1_.id
where
teacherand0_.teacher_id=?
[TeacherAndStudent [teacher=Teacher [id=1, no=000001, name=教师1, sex=女, job_title=初级教师], student=Student [id=1, no=000001, name=学生1, sex=男, birthday=2015-01-27, classId=41]]]
Hibernate:
delete
from
tb_student_teacher
where
teacher_id=?
Hibernate:
insert
into
tb_student_teacher
(teacher_id, student_id)
values
(?, ?)
关闭数据库
5.把多对多分解成两个一对多
事实上,所有的多对多关联都可以分解成两个一对多关联,按照这种方式映射多对多关联,会使域模型和关系数据库模型会有更好的扩展性。下面我再以上的基础上再新建一张表(tb_teacher_student),和一个实体类(StudentTeacher),并对以前的实体类做细微的修改,具体如下。
新建tb_teacher_student表的语句:
CREATE TABLE tb_teacher_student
(
id bigint NOT NULL auto_increment COMMENT 'ID',
student_id bigint COMMENT '学生ID',
teacher_id bigint COMMENT '教师ID',
PRIMARY KEY (id)
) COMMENT = '教师学生关系表';
新建的实体类StudentTeacher:
package model;
public class StudentTeacher
{
private Long id;
private Teacher teacher;
private Student student;
@Override
public String toString()
{
return "StudentTeacher [id=" + id + ", teacher=" + teacher + ", student=" + student + "]";
}
// 省略setter、getter...
}
修改后的实体类:
package model;
import java.sql.Date;
import java.util.HashSet;
import java.util.Set;
public class Student
{
private Long id;
private String no;
private String name;
private String sex;
private Date birthday;
private Long classId;
private Set<StudentTeacher> studentTeachers = new HashSet<StudentTeacher>();
@Override
public String toString()
{
return "Student [id=" + id + ", no=" + no + ", name=" + name + ", sex=" + sex + ", birthday=" + birthday + ", classId=" + classId + "]";
}
// 省略setter、getter...
}
package model;
import java.util.HashSet;
import java.util.Set;
public class Teacher
{
private Long id;
private String no;
private String name;
private String sex;
private String job_title;
private Set<StudentTeacher> studentTeachers = new HashSet<StudentTeacher>();
@Override
public String toString()
{
return "Teacher [id=" + id + ", no=" + no + ", name=" + name + ", sex=" + sex + ", job_title=" + job_title + "]";
}
// 省略setter、getter...
}
对应的配置文件如下,只是用了一对多的双向关联,这里给出代码就不多讲了:
<hibernate-mapping package="model">
<class name="StudentTeacher" table="tb_teacher_student">
<id name="id" column="id">
<generator class="native"></generator>
</id>
<many-to-one name="teacher" column="teacher_id" class="Teacher"/>
<many-to-one name="student" column="student_id" class="Student"/>
</class>
</hibernate-mapping>
<hibernate-mapping package="model">
<class name="Teacher" table="tb_teacher">
<id name="id" column="id">
<generator class="native"></generator>
</id>
<property name="no" column="no" />
<property name="name" column="name" />
<property name="sex" column="sex" />
<property name="job_title" column="job_title" />
<set name="studentTeachers" inverse="true" cascade="save-update">
<key column="teacher_id" />
<one-to-many class="StudentTeacher"/>
</set>
</class>
</hibernate-mapping>
<hibernate-mapping package="model">
<class name="Student" table="tb_student">
<id name="id">
<generator class="native"></generator>
</id>
<property name="no" column="no"/>
<property name="name" column="name"/>
<property name="sex" column="sex"/>
<property name="birthday" column="birthday"/>
<property name="classId" column="class_id"/>
<set name="studentTeachers" inverse="true" cascade="save-update">
<key column="student_id" />
<one-to-many class="StudentTeacher"/>
</set>
</class>
</hibernate-mapping>
测试代码,以及运行结果如下:
......省略打开session的代码
try
{
teacher =(Teacher) session.get(Teacher.class, new Long(1));
student = (Student) session.get(Student.class, new Long(1));
StudentTeacher st=new StudentTeacher();
st.setStudent(student);
st.setTeacher(teacher);
student.getStudentTeachers().add(st);
session.update(student);
transaction.commit();
}
......省略关闭session的代码
连接数据库
Hibernate:
........ 省略若干获取关联对象的查询语句Hibernate: insert into tb_teacher_student (teacher_id, student_id) values (?, ?)
关闭数据库
事实上,所有的多对多关联都可以分解成两个一对多关联,按照这种方式映射多对多关联,会使域模型和关系数据库模型会有更好的扩展性。
6.相关配置详解
(1)set节点配置说明
<set name="EntityClassName"
access="field|property|ClassName"
collection-type="collection-type"
schema="schema"
catalog="catalog"
check="arbitrary sql check condition"
table="TableName"
subselect="SQL expression"
where="arbitrary sql where condition"
sort="unsorted"
optimistic-lock="false|true"
inverse="false|true"
fetch="join|select"
batch-size="5"
cascade="all|none|save-update|delete"
lazy="false|true"
mutable="false|true"
outer-join="false|true"
order-by="arbitrary sql order by condition"
embed-xml="false|true"
persister="PersisterClass"
node="element-name"/>
如上展示了set节点常用的配置,与bag节点基本一样只是多了sort属性是面对其配置进行详细的说明:
- name:实体类属性名。
- access:默认的实体类属性访问模式,取值为property表示访问getter、setter方法间接访问实体类字段,取值为field表示直接访问实体类字段(类成员变量)。
- collection-type:
- schema:数据库schema。
- catalog:数据库catalog。
- check:这是一个SQL表达式,用于为自动生成的schema添加多行约束检查。
- table:此集合里的实体类对应的数据库表名。
- subselect:一个SQL子查询,它将一个不可变并且只读的实体映射到一个数据库的子查询。
- where:一个SQL查询的where条件,获取这个关联类的对象时会一直增加这个条件。
- sort:用来定义集合的排序规则,sort="natural"表示使用对象的comparaTo()方法排序。
- optimistic-lock:指定这个属性在做更新时是否需要获得乐观锁定,默认为true。
- inverse:当设置inverse="true"时,Hibernate将根此集合里的实体类类型的关联属性维护关联关系,默认值false。
- fetch:参数指定了关联对象抓取的方式是select查询还是join查询,默认为select。fetch="join"等同于outer-join="true",fetch="select"等同于outer-join="false"。
- batch-size:用于设置批次操作的SQL语句的数量,默认为1。
- cascade:指明哪些操作会从父对象级联到关联的对象。
- lazy:是否采用延迟加载策略。
- mutable:此集合里的实体类是否会发生改变,如果类实例对应的数据库表记录不会发生更新,可将其设为false,适用于单纯的Insert操作不使用update操作。
- outer-join:设置Hibernate是否使用外连接获取关联的数据,设置成true可以减少SQL语句的条数。
- order-by:一个SQL查询的order by条件,获取这个关联类的对象时会一直增加这个条件。
- embed-xml:如果embed-xml="true",则对应于被关联实体或值类型的集合的XML树将直接嵌入拥有这些关联的实体的XML树中,默认值为true。
- persister:指定持久化实现类,通过指定持久化类,我们可以实现自定义的持久化方法。持久化类为ClassPersister接口的实现。
- node:配置说明。
(2)composite-element节点配置说明
<composite-element class="ClassName
" node="element-name"><parent name="PropertyName
" /><property name="PropertyName
"></property><many-to-one />
</composite-element>
- class:用于设置组件的类型。
- node:配置说明。
- parent name:组件所属的实体类类型,值一般是组件类本身的实体类属性。
- property:组件类属性名。
- many-to-one:用于配置组件类的关联关系,不仅能使用many-to-one元素,还可使用其他的关联映射配置。