MyBatis 关系映射及多表查询
关系类型
表与表之间的关系(relation),分成三种:
- 一对一(one-to-one):一种对象与另一种对象是一一对应关系,比如一个学生只能在一个班级。
- 一对多(one-to-many): 一种对象可以属于另一种对象的多个实例,比如一张唱片包含多首歌。
- 多对多(many-to-many):两种对象彼此都是 "一对多" 关系,比如一张唱片包含多首歌,同时一首歌可以属于多张唱片。

其中,一般通过外键约束它们
- 一对一:在任意一方引入对方主键作为外键。
- 一对多:在 “多” 的一方,添加 “一” 的一方的主键作为外键。(连着多条线的一方是 “多”)
- 多对多:产生中间关系表,引入两张表的主键作为外键,两个主键成为联合主键或使用新的字段作为主键。
MyBatis 加载关联关系对象主要通过两种方式:嵌套查询和嵌套结果。

问题:虽然使用嵌套查询的方式比较简单,但是嵌套查询的方式要执行多条 SQL 语句,这对于大型数据集合和列表展示不是很好,因为这样可能会导致成百上千条关联的 SQL 语句被执行,从而极大的消耗数据库性能并且会降低查询效率。类似暴力 for 循环吧。
解决:MyBatis 延迟加载的配置。使用 MyBatis 的延迟加载在一定程度上可以降低运行消耗并提高查询效率。MyBatis 默认没有开启延迟加载,需要在核心配置文件中的 <settings> 元素内进行配置,具体配置方式如下:
<settings>
<setting name="lazyLoadingEnabled" value="true" />
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
在映射文件中,<association> 元素和 <collection> 元素中都已默认配置了延迟加载属性,即默认属性 fetchType="lazy"(属性 fetchType="eager" 表示立即加载),所以在配置文件中开启延迟加载后,无需在映射文件中再做配置。
懒加载的原理
MyBatis 仅支持 association 关联对象和 collection 关联集合对象的延迟加载。在 MyBatis 配置文件中,可以配置是否启用延迟加载 lazyLoadingEnabled=true|false。(上面介绍了)
它的原理是,使用 CGLIB 创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用 a.getB().getName(),拦截器 invoke() 方法发现 a.getB() 是 null 值,那么就会单独发送事先保存好的查询关联 B 对象的 sql,把 B 查询上来,然后调用 a.setB(b),于是 a 的对象 b 属性就有值了,接着完成 a.getB().getName() 方法的调用。这就是延迟加载的基本原理。
当然了,不光是 MyBatis,几乎所有的包括 Hibernate,支持延迟加载的原理都是一样的。
association 以及 collection 标签
在项目中,某些实体类之间肯定有关键关系,比如一对一,一对多等。在 hibernate 中用 one to one 和 one to many,而 Mybatis 中就用 association 和 collection。
- association:一对一关联(has one)
- collection:一对多关联(has many)
只有在做 select 查询时才会用到这两个标签,它们都有三种用法,且用法类似。
注意:下面所有 resultMap 中的 type、select 标签中的 resultType 以及 association 中的 javaType,collection 中的 ofType,这里只写了类名,是因为在 mybatis-config.xml 中配置了 typeAliases,否则就要写该类的全类名。
<typeAliases>
<packagename="com.example.entity"/>
</typeAliases>
association 的三种用法
一对一:用 <association> 元素处理(这个是用来关联查询的)
- a)、property:指定映射到的实体类对象中的属性,与表字段一一对应。
- b)、column:指定数据库表中对应的字段。
- c)、javaType:指定映射到实体对象属性的类型。
- d)、select:指定引入嵌套查询的子SQL语句,该属性用于关联映射中的嵌套查询。
- e)、fetchType:指定在关联查询时是否启用延迟加载。该属性有lazy和eager两个属性值,默认值为lazy(即默认关联映射延迟加载)。
编写测试环境
创建 Entity
@Data
public class User {
private Integer userId;
private String userName;
private Integer age;
private Card card;//一个人一张身份证,1对1
}
@Data
public class Card {
private Integer cardId;
private String cardNum;//身份证号
private String address;//地址
}
编写 Mapper 接口
public interface UserDao {
/**
* 通过userId查询user信息
* @param userId
* @return
*/
User queryById(int userId);
}
<select id="queryById" parameterType="int" resultMap="userMap">
SELECT u.user_name,u.age,c.card_id,c.card_num,c.address
FROM tb_user u,tb_card c
WHERE u.card_id=c.card_id
AND
u.user_id=#{userId}
</select>
以上是实体类、dao 层的设计以及在 UserDao.xml 中 queryById 方法的 sql 语句的编写,因为不论用 association 的哪种方式, SQL 语句都是一样的写,不同的只是 userMap 的写法,所以这里先给出这段代码。
User 询 Card 是一对一关系,在数据库中,tb_user 表通过外键 card_id 关联 tb_card 表。
下面分别用 association 的三种用法来实现 queryById 方法。
方式一:使用 select 引用
这种方法需要再定义 CardDao.java,如下:
public interface CardDao {
Card queryCardById(int cardId);
}
在 CardDao.xml 中实现该方法:
<select id="queryCardById" parameterType="int" resultType="Card">
SELECT *
FROM tb_card
WHERE card_id=#{cardId}
</select>
然后再看 UserDao.xml 中是如何引用这个方法的:
<resultMap type="User" id="userMap">
<result property="userName" column="user_name"/>
<result property="age" column="age"/>
<association property="card"
column="card_id"
select="com.zhu.ssm.dao.
CardDao.queryCardById">
</association>
</resultMap>
在这里直接通过 select 引用 CardDao 的 queryById 方法。个人感觉这种方法比较麻烦,因为还要在 CardDao 里定义 queryCardById 方法并且实现再引用才有用,不过这种方法 思路清晰,易于理解。
注意:这里的 association 如果要传递多个参数,可以使用以下方式:
<association column="{param1=id,param2=name}" property="User" select="getUser"></association>
<select id="getUser" resultMap="UserMap" parameterType="java.util.Map">
SELECT * FROM user_table WHERE id = #{param1} and name = #{param2}
</select>
association 标签里面的 column 以对象的形式传过去,接收的时候把 parameterType 改为其中 id 和 name 是对应你表的字段,两个 param 名字随便定义
方式二:嵌套 resultMap
<resultMap type="Card" id="cardMap">
<!-- 注意,这个 Map 是通过这个 id 标签查询到的 -->
<id property="cardId" column="card_id"/>
<result property="cardNum" column="card_num"/>
<result property="address" column="address"/>
</resultMap>
<resultMap type="User" id="userMap">
<result property="userName" column="user_name"/>
<result property="age" column="age"/>
<association property="card"
resultMap="cardMap">
</association>
</resultMap>
第二种方法就是在 UserDao.xml 中先定义一个 Card 的 resultMap,然后在 User 的 resultMap 的 association 标签中通过 resultMap="cardMap" 引用。这种方法相比于第一种方法较为简单。(注意要在 Card 的 resultMap 加上 id 标签)
这种嵌套的方法细节可以看这个:Mybatis--collection或association嵌套查询(三层或三层以上)
方法三:嵌套 resultMap 简化版
<resultMap type="User" id="userMap">
<result property="userName" column="user_name"/>
<result property="age" column="age"/>
<association
property="card"
column="card_id"
javaType="Card">
<!-- 同样都是通过这个 id 标签来做关联查询的 -->
<id property="cardId" column="card_id"/>
<result property="cardNum" column="card_num"/>
<result property="address" column="address"/>
</association>
</resultMap>
这种方法就把 Card 的 resultMap 定义在了 association 标签里面,通过 javaType 来指定是哪个类的 resultMap,个人认为这种方法最简单,缺点就是 cardMap 不能复用。具体用哪种方法,视情况而定。
延迟加载
有的同学可能会说,返回的身份证信息我不一定用啊,每次都查询一次数据库,好浪费性能啊,能不能在我使用到身份证信息即获取 card 属性时再去查询数据呢?答案当然是能,那么如何实现呢?
实现延迟加载需要使用 association 标签的 fetchType 属性,该属性有 lazy 和 eager 两个值,分别代表延迟加载和积极加载。
所以上面的配置就要修改成:
<association property="card"
fetchType="lazy"
column="card_id"
select="com.zhu.ssm.dao.
CardDao.queryCardById">
</association>
为了能看到效果,我们在测试方法中添 加一行输出语句:
System.out.println("调用 CardDao.queryCardById()");
Assert.assertNotNull(cardDao.queryCardById());
再次运行测试方法,发现输出日志和预期的不一样,在获取 card 属性前还是查询了2次数据库,这是为什么呢?
这是因为 MyBatis 的全局配置中,有一个 aggressiveLazyLoading 参数,如果这个参 数的值为 true,会使带有延迟加载属性的对象完整加载,如果为 false,则会按需加载,这个参数默认值为 true(3.4.5版本开始默认值改为 false),而截止目前,我们使用的版本为 3.3.1。
所以我们要在 mybatis-config.xml 中添加如下配置:
<settings>
<!--其他配置-->
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
有的同学可能又会说,你现在把全局的 aggressiveLazyLoading 改为了 false,我能不能在触发某个方法时将所有的数据都加载进来呢?答案当然是能,那么如何实现呢?
MyBatis 提供了参数 lazyLoadTriggerMethods,这个参数的含义是,当调用配置中的方法时,加载全部的延迟加载数据,默认值为 “equals,clone,hashCode,toString”。
collection 的三种用法
编写测试环境
例如一个土豪有多个手机
User实体类
@Data
public class User{
private Integer userId;
private String userName;
private Integer age;
private List<MobilePhone> mobilePhone;//土豪,多个手机,1对多
}
手机类
@Data
public class MobilePhone {
private Integer mobilePhoneId;
private String brand;//品牌
private double price;//价格
private User user;//主人
}
dao层
public interface UserDao {
/**
* 通过userId查询user信息
* @param userId
* @return
*/
User queryById(int userId);
}
UserDao.xml 中的 select 查询语句
<select id="queryById" parameterType="int" resultMap="userMap">
SELECT u.user_name,u.age,
m.brand,m.price
FROM tb_user u,tb_mobile_phone m
WHERE m.user_id=u.user_id
AND
u.user_id=#{userId}
</select>
方法一:使用 select 引用
这种方法和上面的有点像
先定义 MobilePhoneDao.java
public interface MobilePhoneDao {
List<MobilePhone> queryMbByUserId(int userId);
}
然后实现该方法 MobilePhoneDao.xml
<resultMap type="MobilePhone" id="mobilePhoneMap">
<id property="mobilePhoneId" column="user_id"/>
<result property="brand" column="brand"/>
<result property="price" column="price"/>
<association property="user" column="user_id"
select= "com.zhu.ssm.dao.UserDao.queryById">
</association>
</resultMap>
<select id="queryMbByUserId" parameterType="int" resultMap="mobilePhoneMap">
SELECT brand,price
FROM tb_mobile_phone
WHERE user_id=#{userId}
</select>
做好以上准备工作,那就可以在 UserDao.xml 中引用了
<resultMap type="User" id="userMap">
<id property="userId" column="user_id"/>
<result property="userName" column="user_name"/>
<result property="age" column="age"/>
<collection property="mobilePhone"
column="user_id"
select="com.zhu.ssm.dao.MobilePhoneDao.queryMbByUserId">
</collection>
</resultMap>
这种方法和 association 的第一种用法几乎是一样的不同之处就是 mobilePhMap 中用到了 association,queryMbByUserId 中要使用 mobilePhoneMap,而不能直接使用 resultType。
方式二:嵌套 resultMap
<resultMap type="MobilePhone" id="mobilephoneMap">
<id column="mobile_phone_id" property="mobilePhoneId"/>
<result column="brand" property="brand" />
<result column="price" property="price" />
</resultMap>
<resultMap type="User" id="userMap">
<result property="userName" column="user_name"/>
<result property="age" column="age"/>
<collection property="mobilePhone" resultMap="mobilephoneMap" >
</collection>
</resultMap>
定义好这两个 resultMap,再引用 UserMap 就行了。
方法三:嵌套 resultMap 简化版
<resultMap type="User" id="userMap">
<result property="userName" column="user_name"/>
<result property="age" column="age"/>
<collection property="mobilePhone"
column="user_id"
ofType="MobilePhone">
<id column="mobile_phone_id" property="mobilePhoneId" />
<result column="brand" property="brand" />
<result column="price" property="price" />
</collection>
</resultMap>
这种方法需要注意,一定要有 ofType,collection 装的元素类型是啥 ofType 的值就是啥,这个一定不能少。
一对一关系
class A{
B b;
}
class B{
A a;
}
//一对一:在本类中定义对方类型的对象,如 A 类中定义 B 类类型的属性 b,B 类中定义 A 类类型的属性 a
举例:以个人和身份证之间的一 对一关联关系
创建测试表
创建两个表 tb_idcard 和 tb_person
USE mybatis;
CREATE TABLE tb_idcard(
id INT PRIMARY KEY AUTO_INCREMENT,
CODE VARCHAR(18)
);
INSERT INTO tb_idcard(CODE) VALUE('152221198711020624');
INSERT INTO tb_idcard(CODE) VALUE('152201199008150317');
CREATE TABLE tb_person(
id INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(32),
age INT,
sex VARCHAR(8),
card_id INT UNIQUE,
FOREIGN KEY(card_id) REFERENCES tb_idcard(id)
);
INSERT INTO tb_person(NAME,age,sex,card_id) VALUE('Rose',29,'女',1);
INSERT INTO tb_person(NAME,age,sex,card_id) VALUE('tom',27,'男',2);
此时表内数据:

创建持久化类
package com.itheima.po;
/**
* 证件持久化类
*/
@Data
public class IdCard {
private Integer id;
private String code;
}
package com.itheima.po;
/**
* 个人持久化类
*/
@Data
public class Person {
private Integer id;
private String name;
private Integer age;
private String sex;
private IdCard card; //个人关联的证件
}
方法一:嵌套查询
IdCardMapper.xml 映射文件:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.mapper.IdCardMapper">
<!-- 根据id查询证件信息,最普通的配置信息 -->
<select id="findCodeById" parameterType="Integer" resultType="IdCard">
SELECT * from tb_idcard where id=#{id}
</select>
</mapper>
PersonMapper.xml 映射文件:
<!-- 嵌套查询:通过执行另外一条SQL映射语句来返回预期的特殊类型 -->
<select id="findPersonById" parameterType="Integer"
resultMap="IdCardWithPersonResult123">
SELECT * from tb_person where id=#{id}
</select>
<!-- resultMap最终还是要将结果映射到pojo上,type就是指定映射到哪一个pojo -->
<resultMap type="Person" id="IdCardWithPersonResult123">
<id property="id" column="id" />
<result property="name" column="name" />
<result property="age" column="age" />
<result property="sex" column="sex" />
<!-- 注意这是在同一个 resultMap 里面 -->
<!-- 一对一:association使用select属性引入另外一条SQL语句,是另一个映射文件的select元素id -->
<association property="card" column="card_id" javaType="IdCard"
select="com.itheima.mapper.IdCardMapper.findCodeById" />
</resultMap>
嵌套查询:测试方法
测试方法:
/**
* 嵌套查询
*/
@Test
public void findPersonByIdTest() {
// 1、通过工具类生成SqlSession对象
SqlSession session = MybatisUtils.getSession();
// 2.使用MyBatis嵌套查询的方式查询id为1的人的信息
Person person = session.selectOne("com.itheima.mapper."
+ "PersonMapper.findPersonById", 1);
// 3、输出查询结果信息
System.out.println(person);
// 4、关闭SqlSession
session.close();
}
运行结果:执行了多条简单的 SQL 语句

方法二:嵌套结果的方式
实际上就是直接查询两个表,合并它们的结果
PersonMapper.xml 映射文件
<!-- 嵌套结果:使用嵌套结果映射来处理重复的联合结果的子集 -->
<select id="findPersonById2" parameterType="Integer"
resultMap="IdCardWithPersonResult2">
SELECT p.*,idcard.code
from tb_person p,tb_idcard idcard
where p.card_id=idcard.id
and p.id= #{id}
</select>
<resultMap type="Person" id="IdCardWithPersonResult2">
<id property="id" column="id" /><!-- 声明主键,id是关联查询对象的唯一标识符 -->
<result property="name" column="name" />
<result property="age" column="age" />
<result property="sex" column="sex" />
<association property="card" javaType="IdCard">
<id property="id" column="card_id" />
<result property="code" column="code" />
</association>
</resultMap>
嵌套结果:测试方法
/**
* 嵌套结果
*/
@Test
public void findPersonByIdTest2() {
// 1、通过工具类生成SqlSession对象
SqlSession session = MybatisUtils.getSession();
// 2.使用MyBatis嵌套结果的方法查询id为1的人的信息
Person person = session.selectOne("com.itheima.mapper."
+ "PersonMapper.findPersonById2", 1);
// 3、输出查询结果信息
System.out.println(person);
// 4、关闭SqlSession
session.close();
}
测试结果:只执行了一条复杂的 SQL 语句。
DEBUG [main] - ==> Preparing: SELECT p.*,idcard.code from tb_person p,tb_idcard idcard where p.card_id=idcard.id and p.id= ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
Person [id=1, name=Rose, age=29, sex=女, card=IdCard [id=1, code=152221198711020624]]
实际上就是直接查询两个表,合并 它们的结果
select p.*,idcard.code from tb_person p,tb_idcard idcard where p.card_id = idcard.id and p.id = #{id}
一对一关系配置模板

一对多关系
class A{
List<B> b;
}
class B{
A a;
}
//一对多:就是一个 A 类类型中对应多个 B 类类型的情况,
// 需要在 A 类中以集合的方式引入 B 类类型的对象,在 B 类中定义 A 类类型的属性 a
一对多关系就是 <resultMap> 元素中,包含一个子元素 <collection> 元素,属性大部分和 <association> 元素相同,但有一个特殊属性 ofType,这个属性和 javaType 属性对应,用于指定实体对象中集合类属性所包含的元素类型。
一对多关系配置模板
<collection> 元素的使用模板:
