在Hibernate中,两个相互有关联的数据库表在配置的时候需要额外的配置。
关系映射之多对一
多对一的经典案例:老师 <–> 部门,即一个老师只能对应一个部门,而一个部门对应多个老师。
下面通过示例说明多对一的关系。创建项目HibernateManyToOne
。
1.首先创建Teacher
对象和Department
对象。
package com.gavin.domain;
public class Teacher {
private Integer id;
private String name;
private Department department;
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 Department getDepartment() {
return department;
}
public void setDepartment(Department department) {
this.department = department;
}
}
package com.gavin.domain;
import java.util.Set;
public class Department {
private Integer id;
private String name;
private Set<Teacher> teacherSet;
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<Teacher> getTeacherSet() {
return teacherSet;
}
public void setTeacherSet(Set<Teacher> teacherSet) {
this.teacherSet = teacherSet;
}
}
2.接着配置两个domain对象的对象关系映射文件,Teacher.hbm.xml和Department.hbm.xml。
Department.hbm.xml
文件如下:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD//EN"
"http://hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="com.gavin.domain">
<class name="com.gavin.domain.Department" table="department">
<!--配置主键属性-->
<id name="id" column="did" type="java.lang.Integer">
<generator class="increment"/>
</id>
<property name="name" column="name" length="64" not-null="true"/>
<set name="teacherSet">
<key column="did"/>
<one-to-many class="com.gavin.domain.Teacher"/>
</set>
</class>
</hibernate-mapping>
可以看到,在Department的对象关系映射文件中,我们配置了<set>
属性,name为teacherSet
,正是Department对象中的集合属性名,这个set
属性代表着一对多,它正是这个“多”存放的集合。接下来的<key column = 'did'>
指定了Teacher对象引用自己的字段为did
。紧接着配置了<one-to-many>
,其class
属性指定了一对多的“多”指的是哪一个对象。
另外,通过<generator class="increment">
配置了主键的增长策略为自增长。即我们在保存该对象的时候,可以不给其指定id,Hibernate会自动查出下一个id。
Teacher.hbm.xml
文件如下:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD//EN"
"http://hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="com.gavin.domain">
<class name="com.gavin.domain.Teacher" table="teacher">
<!--配置主键属性-->
<id name="id" column="sid" type="java.lang.Integer">
<generator class="increment"/>
</id>
<property name="name" type="java.lang.String" column="name" length="64" not-null="true"/>
<!---->
<many-to-one name="department">
<!--column表示将来映射出来的表的外键名-->
<column name="did"/>
</many-to-one>
</class>
</hibernate-mapping>
可以看到与Department.hbm.xml
相反,其配置了<many-to-one>
,也就是多个Teacher
对应一个Deparment
。
3.配置Hibernate
hibernate.cfg.xml文件如下:
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD//EN"
"http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<property name="connection.url">jdbc:mysql://localhost:3306/hibernate</property>
<property name="connection.driver_class">com.mysql.jdbc.Driver</property>
<property name="show_sql">true</property>
<property name="connection.username">root</property>
<property name="connection.password">gavin</property>
<!-- DB schema will be updated if needed -->
<property name="hbm2ddl.auto">update</property>
<mapping resource="com/gavin/domain/Department.hbm.xml"/>
<mapping resource="com/gavin/domain/Teacher.hbm.xml"/>
</session-factory>
</hibernate-configuration>
其中,我们配置了<property name="hbm2ddl.auto">update</property>
,让Hibernate自动为我们生成数据库表。
并且配置了<property name="show_sql">true</property>
,可以在控制台输出Hibernate使用的SQL语句。
4.测试案例
import com.gavin.domain.Department;
import com.gavin.domain.Teacher;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;
public class Main {
public static void main(String[] args) {
SessionFactory sessionFactory = new Configuration().configure().buildSessionFactory();
Session session = sessionFactory.openSession();
Transaction transaction = null;
try {
transaction = session.beginTransaction();
// 创建一个Department对象
Department department = new Department();
department.setName("IT Department");
// 创建一个Teacher对象
Teacher teacher = new Teacher();
teacher.setName("Gavin");
// 设置其Department
teacher.setDepartment(department);
session.save(department);
session.save(teacher);
transaction.commit();
session.close();
} catch (Exception e) {
if (transaction != null) {
transaction.rollback();
}
throw new RuntimeException(e);
}
}
}
执行之后,控制台输出的语句如下:
Hibernate: create table department (did integer not null, name varchar(64) not null, primary key (did)) engine=InnoDB
Hibernate: create table teacher (sid integer not null, name varchar(64) not null, did integer, primary key (sid)) engine=InnoDB
Hibernate: alter table teacher add constraint FKadd6hrh8hv02jw7uss3wra6gb foreign key (did) references department (did)
Hibernate: select max(did) from department
Hibernate: select max(sid) from teacher
Hibernate: insert into department (name, did) values (?, ?)
Hibernate: insert into teacher (name, did, sid) values (?, ?, ?)
从SQL语句中可以看到,Hibernate先创建了两张表department和teacher,然后给teacher表添加了外键引用。紧接着由于我们设置了ID的自增策略,所以Hibernate查询了数据库中的ID的最大值,最后保存了两个对象,向数据库添加了两条记录。
此时数据库的变化如下:
增加了department
表和teacher
表,并且两个表都增加了一条记录。
懒加载问题
因为在这里Teacher和Department有关联关系,如果我们从数据库获取一个Teacher对象,那么它对应的Department对象不会立马读出来,只有当你使用的时候才会读出来给你。
比如下例:
SessionFactory sessionFactory = new Configuration().configure().buildSessionFactory();
Session session = sessionFactory.openSession();
Teacher teacher = session.load(Teacher.class, 1);
System.out.println("teacher.getName() = " + teacher.getName());
此时hibernate的show_sql输出的sql语句如下:
Hibernate: select teacher0_.sid as sid1_1_0_, teacher0_.name as name2_1_0_, teacher0_.did as did3_1_0_ from teacher teacher0_ where teacher0_.sid=?
此时只是查出了对应的Teacher对象,而其对应的Department对象并没有通过数据库读取。只能当你调用这个Department对象的时候才会再次读取,这就是所谓的“懒加载”。如下例子,我们再访问该Teacher对象的Department的名字:
SessionFactory sessionFactory = new Configuration().configure().buildSessionFactory();
Session session = sessionFactory.openSession();
Teacher teacher = session.load(Teacher.class, 1);
System.out.println("teacher.getName() = " + teacher.getName());
System.out.println("teacher.getDepartment().getName() = " + teacher.getDepartment().getName());
此时的sql语句为:
Hibernate: select teacher0_.sid as sid1_1_0_, teacher0_.name as name2_1_0_, teacher0_.did as did3_1_0_ from teacher teacher0_ where teacher0_.sid=?
Hibernate: select department0_.did as did1_0_0_, department0_.name as name2_0_0_ from department department0_ where department0_.did=?
可以看出比刚刚多了一条语句,也就验证了Hibernate的确是采用懒加载的方式加载关联对象的。
懒加载也会涉及到一个问题,就是当你Session关闭了之后,对关联对象的查询就会出异常了。也就是说当你要使用关联对象时,Session必须不能关闭,此时Hibernate才能从数据库中查出来。
比如下例,我们通过一个方法获取一个Teacher对象,然后在主方法中调用它:
public static Teacher getTeacher(int id) {
SessionFactory sessionFactory = new Configuration().configure().buildSessionFactory();
Session session = sessionFactory.openSession();
Teacher teacher = session.load(Teacher.class, id);
session.close(); // 这里关闭了Session
return teacher;
}
public static void main(String[] args) {
Teacher teacher = getTeacher(1);
System.out.println("teacher.getName() = " + teacher.getName());
System.out.println("teacher.getDepartment().getName() = " + teacher.getDepartment().getName());
}
第一行的输出是完全正常的,teacher.getName()
可以正常获取,因为Name只是Teacher对象的一般属性,但是第二句话teacher.getDepartment().getName()
就会出问题了,Department是Teacher对象的对象属性,如果采用懒加载的话,getTeacher()
方法返回的Teacher对象中还没有包含Department对象,而且此时Session已经关闭了,所以Hibernate也无法再查出来了。出错就是意料之中的了。
解决这个问题的方法有两种:
- 第一种方法是通过显示地初始化代理对象
此时将getTeacher()方法更改如下:
public static Teacher getTeacher(int id) {
SessionFactory sessionFactory = new Configuration().configure().buildSessionFactory();
Session session = sessionFactory.openSession();
Teacher teacher = session.load(Teacher.class, id);
// 初始化代理对象
Hibernate.initialize(teacher.getDepartment());
session.close(); // 这里关闭了Session
return teacher;
}
即在加载出Teacher对象的时候,通过Hibernate.initialize(teacher.getDepartment())
方法显示地初始化Department对象,此时与该Teacher关联的Department对象也会从数据库中读出来。
- 第二种方法就是让Hibernate禁用懒加载
即在代理对象的对象关系映射文件中将该对象设置为禁止懒加载,比如这里的话,我们就在Department.hbm.xml文件中在class中加上lazy='false'
,配置如下:
<class name="com.gavin.domain.Department" lazy="false" table="department">
关系映射之一对多
一对多与上述多对一是相对应的。老师<-->部门
是多对一,那么部门<-->老师
就是一对多,一对多的话需要在部门中配置相关集合(或列表)来保存所谓的“多”个对象,比如Set
,在其Department.hbm.xml中也需要相应的配置:
public class Department implements Serializable{
private int id;
private String name;
private Set<Teacher> set;
//...省略setter和getter
}
<hibernate-mapping package="com.gavin.domain">
<class name="com.gavin.domain.Department" lazy="false" table="department">
<!--配置主键属性-->
<id name="id" column="did" type="java.lang.Integer">
<generator class="increment"/>
</id>
<property name="name" column="name" length="64" not-null="true"/>
<!--这里就是配置的集合,即one-to-many关系-->
<!--name表示集合名字-->
<set name="teacherSet">
<!--指定Teacher类对应的外键-->
<key column="did"/>
<!--class指的是一对多的多对应的是哪一个对象-->
<one-to-many class="com.gavin.domain.Teacher"/>
</set>
</class>
</hibernate-mapping>
当配置好了一对多的关系之后,查询集合就非常方便了,比如这里我们查询属于部门1的所有老师:
public static void main(String[] args) {
SessionFactory sessionFactory = new Configuration().configure().buildSessionFactory();
Session session = sessionFactory.openSession();
Department department = session.load(Department.class, 1);
for (Teacher teacher : department.getTeacherSet()) {
System.out.println("teacher = " + teacher);
}
}
级联操作
同时呢,我们也可以通过级联保存在保存部门的同时,也保存该部门对应的老师,当然级联保存也需要相应的配置,如下的cascade属性:
<set name="set" cascade="save-update">
<key column="did"/>
<one-to-many class="com.gavin.domain.Teacher"/>
</set>
此时我们做如下添加测试:
public static void main(String[] args) {
SessionFactory sessionFactory = new Configuration().configure().buildSessionFactory();
Session session = sessionFactory.openSession();
Transaction transaction = session.beginTransaction();
Department department = new Department();
department.setName("HR Department");
Teacher teacher1 = new Teacher();
teacher1.setName("XiaoMing");
Teacher teacher2 = new Teacher();
teacher2.setName("DaMing");
Set<Teacher> set = new HashSet<>();
set.add(teacher1);
set.add(teacher2);
department.setTeacherSet(set);
session.save(department);
transaction.commit();
}
运行之后的SQL语句输出为:
Hibernate: select max(did) from department
Hibernate: select max(sid) from teacher
Hibernate: insert into department (name, did) values (?, ?)
Hibernate: insert into teacher (name, did, sid) values (?, ?, ?)
Hibernate: insert into teacher (name, did, sid) values (?, ?, ?)
Hibernate: update teacher set did=? where sid=?
Hibernate: update teacher set did=? where sid=?
查看数据库后,发现确实添加进去了:
详细说明:
cascade用来说明当对主对象进行某种操作时是否对其关联的从对象也做类似的操作(比如在bbs项目中,主贴删除之后回帖肯定也就删除了)。
常用的cascade属性有:
none,all,save-update,delete,lock,refresh,evict,replicate,persist,merge,delete-orphan(one-to-may)
一般对many-to-one,many-to-many不设置级联,在one-to-one和one-to-many中设置级联。
- 在集合属性和普通属性中都能使用cascade
- 一般将cascade配置在one-to-many中的one的一方,和one-to-one中的主对象一方
案例:在上述案例的基础上配置级联操作,当删除一个部门的时候,同时也删除该部门的所有老师。
即在casecade
属性上加上delete
这个值,中间用逗号隔开。
<!--当保存或删除部门对象时,同时保存或删除该部门的老师-->
<set name="set" cascade="save-update,delete">
<key column="did"/>
<one-to-many class="com.gavin.domain.Teacher"/>
</set>
上述配置,可以在我们删除或者保存一个部门对象时候,同时删除或者保存这个部门的老师。