Appearance
多表关联概述
表与表之间有哪些关系
- 1:1 一对一(一般情况下,只使用一个表就好)
- 1:N 一对多
- N:1 多对一
- N:N 多对多
数据库本身是不支持多对多的业务。如果遇到多对多,必须拆分一个中间表!!因为数据表每个字段只能保存单个数据,不能保存集合。表与表之间的关系是由需求决定!!
多表关联查询的概念
所谓多表关联查询,就是查询多个表的数据,使用一个实体类对象封装起来。实现这个操作必须做两个步骤:
- 创建一个可以存储多个表数据的实体类
- 将查询的多个表的数据封装这个实体类的对象里面
多表关联查询实现的方案:
- 手工关联的多表关联查询(JDBC)
- 自动的关联多表关联查询(Hibernate)
重要的注意事项:表与表直接的关联关系是通过外键建立的!!!
创建实体类的注意事项
在创建实体类的成员变量,建议使用包装类,因为数据库里面的字段是可以设置为 null 值,而基础数据类型是不能接受 null 值。如:private Long id;
使用 Hibernate 实现自动的多表关联查询
概述
Hibernate 多表关联查询,表与表的关系不需要手动写代码关联,可以通过配置实现自动关联。
注意:通过 JDBC 的配置,可以知道表与表之间的关系是通过外键建立的。所以,不管之前的 JDBC 手工的代码编写,还是后面的 Hibernate 自动关联的配置,离不开的都是外键。
多表关联查询配置步骤
- hibernate 外键机制:关联的双方维护外键,在一和多两边都配置外键。
- 一对多、多对一、多对多,配置的关键是外键。
配置步骤:
- 创建一个可以存储多个表数据的实体。
- 通过配置来实现,将查询的多个表的数据封装这个实体类的对象中。
- 从表的映射文件。注意:不要忘了在 Hibernate.cfg.xml 里面加载。
一对多配置
在一对多中,实体类外键的成员变量一般会使用 Set 集合封装,hibernate 推荐使用 Set 集合。
映射配置项
在实体类映射配置文件中对应使用 <set> 标签:用于指定实体类里面 Set 集合的属性。
<set> 标签的属性解析:
name:值是实体类中表示外键的成员变量名。name="实体类中一对多引用集合属性名",如,name="linkmans"cascade:配置支持级联操作。可选值:save-update:表示支持保存和更新delete:支持级联删除注:如果需要同时支持,使用逗号隔开。如
save-update,delete
<set> 标签内的必须有的子属性标签:
<key>:必须配置外键,因为表与表的关系都是通过外键关联。相关标签属性如下:column:外键名,如column="lkm_cust_id"
<one-to-many>:声明表与表关联关系。注意,声明关联关系时,必须要指定集合元素的类型。相关标签属性如下:class:外键表对应的类全名。如class="com.moon.entity.Linkman"
示例
java
// 客户表和联系人表的关系是一对多,一条客户表的记录对应多条联系人表的记录
private Set<Linkman> linkmans = new HashSet<Linkman>();
public Set<Linkman> getLinkmans() {
return linkmans;
}
public void setLinkmans(Set<Linkman> linkmans) {
this.linkmans = linkmans;
}映射配置文件:
xml
<!--
在客户映射文件中,表示所有联系人
使用set标签表示所有联系人
set标签中的name属性,属性值为实体类表示多个外键的集合
-->
<set name="linkmans">
<!-- 一对多建表,有外键
hibernate外键机制:双身维护外键,在一和多两边都配置外键
column属性值:外键的名称
-->
<!-- 必须配置外键,因为表与表的关系都是通过外键关联 -->
<key column="lkm_cust_id"></key>
<!-- one-to-many 标签:作用声明表与表的关系 -->
<!-- class属性是外键集合的对应类的全路径 -->
<one-to-many class="day60.entiry.Linkman"/>
</set>多对一配置
映射配置项
同一个映射配置文件里面,如果出现了两个以上相同的 column,在更新的时候和插入的时候,不知道以哪个字段的值为准。解决方案如下:
- 方式一:删除一个外键字段。
- 方式二:将一个外键字段设置为,插入和更新的时候忽略它的值
insert="false"和update="false"
推荐使用直接注释外键属性的配置,即方式一。
方式一
在映射配置文件中,先将原本外键的 <property> 属性标签删除。在实体类映射配置文件中对应使用 <many-to-one> 标签:只能用于多对一的关系
<many-to-one> 标签中的属性解析:
name:指定多对一的属性名,引用的属性名。name="实体类中多对一引用属性名",如name="customer"column:关联外键的字段。column="外键名",如column="lkm_cust_id"
使用此方式设置,如果在增加数据时,需要创建外键对应的对象,再设置其外键的值。
方式二
保留原本外键的 <property> 属性标签。在实体类映射配置文件中对应 <many-to-one> 标签中,增加属性 insert="false" 和 update="false"。
示例
- 创建实体类
java
// 一个客户对应多个联系人
private Customer customer;
public Customer getCustomer() {
return customer;
}
public void setCustomer(Customer customer) {
this.customer = customer;
}- 配置方式一:注释原本与后面设置的数据库字段c olumn
xml
<!-- <property name="lkmCustid" column="lkm_cust_id"></property> -->
<!--
配置多对一关系
many-to-one:用于只能多对一的关系
name:指定多对一的属性名,引用的属性名
column:对于关联外键的字段lkm_cust_id
-->
<many-to-one name="customer" column="lkm_cust_id"></many-to-one>- 配置方式二:
xml
<property name="lkmCustid" column="lkm_cust_id"></property>
<many-to-one name="customer" column="lkm_cust_id" insert="false" update="false"></many-to-one>多对一不同配置的操作区别示例:
java
public void save() {
// 获取session操作对象
Session session = HibernateUtil.getSession();
// 开启事务
Transaction transaction = session.beginTransaction();
// 使用方式一新增数据:保留一个数据库外键字段列
Linkman l = new Linkman();
// l.setLkmName("方式一");
// Customer c = new Customer();
// c.setCustId(3L);
// l.setCustomer(c);
// 使用方式二新增数据:保留两个数据库外键字段列
l.setLkmName("方式二");
l.setLkmCustid(19L);
// 保存数据
session.save(l);
// 提交事务
transaction.commit();
// 关闭资源
session.close();
}多对多配置
映射配置项
多对多通常是通过一个表找到另一个表的数据,一般有3个条件(重点)
- 中间表
- 中间表对应本表的外键
- 中间表对应关联表的外键
在多对多中,实体类外键的成员变量一般会使用 Set 集合封装,hibernate 推荐使用 Set 集合。
在实体类映射配置文件中对应使用<set>标签:用于指定实体类里面 Set 集合的属性。
<set> 标签的属性说明(设置中间表):
name:值是实体类中表示外键的成员变量名。name="实体类中一对多引用集合属性名",如name="roles"table:设置多对多关联的中间表。table="中间表名",如table="sys_user_role"cascade:配置支持级联操作。可选值如下:save-update:表示支持保存和更新delete:支持级联删除如果需要同时支持,使用逗号隔开。如
save-update,delete
<set> 标签内的必须有的子属性标签:
<key>:必须配置本实体类对应表在中间表的外键,因为表与表的关系都是通过外键关联。相关标签属性如下:column:外键名,如column="user_id"
<many-to-many>:声明表与表关联关系。注意:声明关联关系时,必须要指定集合元素的类型。相关标签属性如下:class:外键表对应的类全名。如class="com.moon.entity.Role"column:配置关联表在中间表对应的外键。如column="role_id"
示例
- 修改用户实体类
java
// 用户和角色是多对多的关系,一个用户可以有多个角色
private Set<Role> roles = new HashSet<Role>();
public Set<Role> getRoles() {
return roles;
}
public void setRoles(Set<Role> roles) {
this.roles = roles;
}- 修改 user 配置文件
xml
<!--
多对多的关系
1.中间表
2.中间表对应本表的外键
3.中间表对应关联表的外键
-->
<set name="roles" table="sys_user_role">
<!-- 本实体类对应表在中间表的外键 -->
<key column="user_id"></key>
<!--
声明多对多关系
class:本类指定集合的元素对应类的类全名
column:配置关联表在中间表对应的外键
-->
<many-to-many class="day60.entity.Role" column="role_id"></many-to-many>
</set>- 测试类
java
public void get() {
// 获取session操作对象
Session session = HibernateUtil.getSession();
// 查询User对象
User user = session.get(User.class, 3L);
// 输出查询结果
System.out.println("用户名:" + user.getUserName());
for (Role role : user.getRoles()) {
System.out.println("用户的角色是:" + role.getRoleName());
}
// 关闭资源
session.close();
}多表关联操作
级联操作:Hibernate 支持根据配置的关联关系,实现通过一个实体类的对象,可以操作多个表的数据(增删改)。具体的实现是在多表关联配置中,设置外键<set>标签的cascade的属性,配置其支持级联操作,可选值如下:
save-update:表示支持保存和更新delete:支持级联删除
如果需要同时支持,则使用逗号隔开,如 save-update,delete
Notes: 其中级联删除和级联更新都必须使用持久态对象。
级联增加
在映射配置文件中,配置关联关系支持级联增加:
xml
<set name="linkmans" cascade="save-update,delete">测试代码:
java
// 需求:通过Customer的对象插入客户信息和对应的联系人信息!
public void save() {
// 获取session操作对象
Session session = HibernateUtil.getSession();
// 开启事务
Transaction transaction = session.beginTransaction();
// 创建客户对象
Customer c = new Customer();
c.setCustName("级联增加");
// 创建联系人
Linkman l1 = new Linkman();
Linkman l2 = new Linkman();
l1.setLkmName("级联增加联系人1");
l2.setLkmName("级联增加联系人2");
c.getLinkmans().add(l1);
c.getLinkmans().add(l2);
// 增加数据
session.save(c);
// 提交事务
transaction.commit();
// 关闭资源
session.close();
}注意:级联增加必须外键字段设置为空。

因为 Hibernate 级联增加的顺序(执行顺序可以在控制台SQL语句查看)是:
- 先插入主键表的主键。
- 然后插入外键表的数据。
- 最后在将主键表的主键值更新到外键键字段。
- 如果外键不能为 null,会导致第 2 步,插入外键表的数据的时候就报错了。
所以外键字段一定要设置为 null。
级联删除(不建议使用)
级联删除就是通过删除一个主键表的记录,同时也删除掉外键表关联的记录。基于上面示例,已配置 cascade 属性
java
// 需求:通过Customer的对象删除客户信息和对应的联系人信息!
public void delete() {
// 获取session操作对象
Session session = HibernateUtil.getSession();
// 开启事务
Transaction transaction = session.beginTransaction();
// 通过id获取持久化对象
Customer c = session.get(Customer.class, 24L);
// 增加数据
session.delete(c);
// 提交事务
transaction.commit();
// 关闭资源
session.close();
}级联更改(不常用,一般都是直接修改表)
注:基于上面示例,已配置 cascade 属性
java
// 需求:通过Customer的对象删除客户信息和对应的联系人信息!
public void update() {
// 获取session操作对象
Session session = HibernateUtil.getSession();
// 开启事务
Transaction transaction = session.beginTransaction();
// 通过id获取持久化对象
Customer c = session.get(Customer.class, 19L);
// 获取set集合的迭代器
Iterator<Linkman> iterator = c.getLinkmans().iterator();
// 将迭代器指针指向下一个元素,如果有值,则更新
if (iterator.hasNext()) {
iterator.next().setLkmGender("妖");
}
// 增加数据
session.update(c);
// 提交事务
transaction.commit();
// 关闭资源
session.close();
}延迟加载的抓取策略
默认情况是多表关联查询是支持延迟抓取数据的。
创建多个表的关联关系时,只要不使用关联的表是不会查找这个表的。当使用的该表时才查询,不使用的表不查询,这种情况称为延迟加载。
结论:如果使用了延迟加载,不管配置了多少个表的关联,只要不访问它的数据,就不会查询数据库。不会降低查询的效率!!
如果不需要延迟加载,设置<set>标签的属性 lazy="false",无论查询单表还是多表,都多个表都一起查询。
总结:其实是根据连接的两个表,一起出现的一起使用概率来判断的是否使用延迟加载。
- 如果两个表的数据基本都是一起使用的。那么使用非延迟加载。
- 如果两个表的数据分开使用的情况多于一起使用的情况,那么使用延迟加载。
- 如果出现,有时候使用一个单表,有时候使用两个表连接查询,此时只能通过使用的概率分析了。
- 延迟加载是开发完以后才配置的。
导航查询
导航查询概述
导航查询:用于持久化对象。如果表与表之间建立的关联关系,可以一级一级往下访问。
java
/**
* 导航查询测试
*/
public void findAllByNav() {
// 获取session操作对象
Session session = HibernateUtil.getSession();
// 获取HQL操作对象
Query query = session.createQuery("from User");
List<User> list = query.list();
for (User user : list) {
System.out.println("用户名:" + user.getUserName());
// 使用导航查询:用于持久化对象。
// 如果表与表之间建立的关联关系,可以一级一级往下访问。
// 出现N+1次查询问题
for (Role role : user.getRoles()) {
System.out.println("角色名:" + role.getRoleName());
}
}
// 关闭资源
session.close();
}N+1 的问题的解决方案(待补充)
使用导航查询的存在问题:每一条记录都查询一次数据库。所以,如果一条数据查询一次,五十条数据就查询 了50 次数据库,就是 N+1 问题。如果要解决 N+1 的问题,需要通过 HQL 连接表查询。
值得注意的是,HQL 连接表查询必须别表与表之间的关系,使用 HQL 连接表查询是不需要关联条件的,因为关联条件在配置文件已经配置好了,但查询的数据必须要去重复!
使用的关键字:
fetch:就是将数据赋予对应的对象,如果不使用经关键字,导致返回的是一个Object[]。distinct:返回两个表多条的数据时,出现重复的数据。
java
/**
* 使用HQL查询解决N+1问题测试
*/
public void findAllByHQL() {
// 获取session操作对象
Session session = HibernateUtil.getSession();
// 获取HQL操作对象
// 注意,查询的数据必须去重复
// [可选]fetch:就是将数据赋予对应的集合
Query query = session.createQuery("select distinct u from User u inner join fetch u.roles");
List<User> list = query.list();
for (User user : list) {
System.out.println("用户名:" + user.getUserName());
for (Role role : user.getRoles()) {
System.out.println("角色名:" + role.getRoleName());
}
}
// 关闭资源
session.close();
}多表关联相关注意事项
配置多表关联映射文件相关注意事项
一对多和多对一,配置的的都是外键,不用配置主键。因为主键本身就有一个单独的标签 <id>,标签是唯一的。而外键没有单独的标签的。
- 必须知道为什么导致字段冲突。因为出现两个属性对应一个数据库的字段(column)。
- 必须知道如何解决字段冲突的问题。
保留一个单独的属性 <property>,用于插入和更新使用的外键,缺陷导致级联增加外键字段值为 null。
使用多对一的关联关系用于插入和更新的外键属性,缺陷导致每次插入数据都要 new 一个关联对象。
- 如果要保证功能的完整性,需要支持级联增加,使用多对的关系作为外键属性。
- 如果不需要级联增加,那么就可以使用一个属性单独作为外键
不建议使用 List 类型做实体类封装外键
List 类型的配置也与 Set 类型的配置基本是一致,将实体类映射配置<set>标签改成<list>。
但不推荐使用 List,其原因是,List 集合必须要在数据库里面的外键表增加一个排序字段来排列数据。这样导致,插入数据和查询数据,都很麻烦。
Notes: Hibernate 配置 list 必须要在数据库外键表指定一个排序字段,在
<list>标签中增加属性标签。
xml
<index column="sort"></index>还需注意,如果排序列中出现了断号(即顺序不连续),list 集合会将断号的对应的索引号的值都设置为 null,所以遍历 list 集合时必须判断是否为空。示例如下:
java
// 用户和角色是多对多的关系,一个用户可以有多个角色
private List<Role> roles = new ArrayList<Role>();
public List<Role> getRoles() {
return roles;
}
public void setRoles(List<Role> roles) {
this.roles = roles;
}映射配置:
xml
<list name="roles" table="sys_user_role">
<!-- 本实体类对应表在中间表的外键 -->
<key column="user_id"></key>
<!-- list集合必须指定哪个字段为排序字段 -->
<index column="sort"></index>
<!--
声明多对多关系
class:本类指定集合的元素对应类的类全名
column:配置关联表在中间表对应的外键
-->
<many-to-many class="day60.entity.Role" column="role_id"></many-to-many>测试:
java
public void get() {
// 获取session操作对象
Session session = HibernateUtil.getSession();
// 查询User对象
User user = session.get(User.class, 3L);
// 输出查询结果
System.out.println("用户名:" + user.getUserName());
for (Role role : user.getRoles()) {
if (role != null) {
System.out.println("用户的角色是:" + role.getRoleName());
}
}
// 关闭资源
session.close();
}多建一列用作排序:

一对多、多对一、多对多实体类重写 toString 方法
在建立实体类关系后,重写 toString 方法时,不能将同时将两个外键写到 toString 方法,否则会出现死循环互相调用。