4 MyBatis的关联映射和缓存机制
约 13651 字大约 46 分钟
2025-12-20
学习目标
- 了解数据表之间的3种关联关系
- 了解对象之间的3种关联关系
- 熟悉关联关系中的嵌套查询和嵌套结果
- 掌握一对一关联映射
- 掌握一对多关联映射
- 掌握多对多关联映射
- 熟悉MyBatis的缓存机制
前面几章介绍了MyBatis的基本用法、关联映射和动态SQL等重要知识。学习完前面几章后,读者已经能够使用MyBatis以面向对象的方式进行数据库操作了,但这些操作只是针对单表实现的。在实际开发中,对数据库的操作常常会涉及多张表,针对多表之间的操作,MyBatis提供了关联映射,通过关联映射可以很好地处理表与表、对象与对象之间的关联关系。此外,在实际开发中经常需要合理利用MyBatis缓存来加快数据库查询,进而有效地提升数据库性能。
4.1 关联映射概述
在关系型数据库中,表与表之间存在着3种关联映射关系,分别为一对一、一对多和多对多,如图4-1所示。

图4-1 表与表之间的3种关联映射关系
这3种关联映射关系的具体说明如下:
- 一对一:一个数据表中的一条记录最多可以与另一个数据表中的一条记录相关。例如,现实生活中学生与校园卡就属于一对一的关系,一个学生只能拥有一张校园卡,一张校园卡只能属于一个学生。
- 一对多:主键数据表中的一条记录可以与另外一个数据表的多条记录相关,但另外一个数据表中的记录只能与主键数据表中的某一条记录相关。例如,现实生活中班级与学生的关系就属于一对多的关系,一个班级可以有很多学生,但一个学生只能属于一个班级。
- 多对多:一个数据表中的一条记录可以与另外一个数据表任意数量的记录相关,另外一个数据表中的一条记录也可以与本数据表中任意数量的记录相关。例如,现实生活中学生与教师就属于多对多的关系,一名学生可以由多名教师授课,一名教师可以为多名学生授课。
数据表之间的关系实质上描述的是数据之间的关系,除了数据表,在Java中还可以通过对象描述数据之间的关系。通过Java对象描述数据之间的关系,其实就是使对象的属性与另一个对象的属性相互关联。Java对象描述数据之间的关系示意图如图4-2所示。

图4-2 Java对象描述数据之间的关系示意图
在图4-2中,3种Java对象关联映射关系的描述如下:
- 一对一:就是在本类中定义与之关联的类的对象作为属性。例如,在A类中定义B类对象b作为属性,在B类中定义A类对象a作为属性。
- 一对多:就是一个A类对象对应多个B类对象的情况。例如,在A类中定义一个B类对象的集合作为A类的属性;在B类中定义A类对象a作为B类的属性。
- 多对多:在两个相互关联的类中,都可以定义多个与之关联的类的对象。例如,在A类中定义B类对象的集合作为A类的属性,在B类中定义A类对象的集合作为B类的属性。
4.2 一对一查询
在现实生活中,一对一关联关系是十分常见的。例如,一个人只能有一个身份证,同时一个身份证也只对应一个人。人与身份证之间的关联关系如图4-3所示。

图4-3 人与身份证之间的关联关系
在MyBatis中,通过 <association> 元素来处理一对一关联关系。<association> 元素提供了一系列属性用于维护数据表之间的关系。<association> 元素中的属性如表4-1所示。
表4-1**<association>**元素中的属性
| 属性 | 说明 |
|---|---|
property | 用于指定映射到的实体类对象的属性,与表字段一一对应 |
column | 用于指定表中对应的字段 |
javaType | 用于指定映射到实体对象的属性的类型 |
jdbcType | 用于指定数据表中对应字段的类型 |
fetchType | 用于指定在关联查询时是否启用延迟加载。fetchType属性有lazy和eager两个属性值,默认值为**lazy**(即默认关联映射延迟加载) |
select | 用于指定引入嵌套查询的子SQL语句,该属性用于关联映射中的嵌套查询 |
autoMapping | 用于指定是否自动映射 |
typeHandler | 用于指定一个类型处理器 |
<association> 元素是 <resultMap> 元素的子元素,其使用非常简单,它有两种配置方式,即嵌套查询和嵌套结果,下面对这两种配置方式分别进行介绍。
1. 嵌套查询
嵌套查询就是在一个SQL语句里再跑另一个SQL语句,这样可以得到更复杂的结果。具体的设置方法如下:
<!--方式一:嵌套查询-->
<association property="card" column="card_id"
javaType="com.itheima.pojo.Idcard"
select="com.itheima.mapper.IdCardMapper.findCodeById" />2. 嵌套结果
嵌套结果就是用来整理那些重复出现的数据部分的方法。通过一种叫做嵌套结果映射的方式来实现。具体的设置方法如下所示:
<!--方式二:嵌套结果-->
<association property="card" javaType="com.itheima.pojo.IdCard">
<id property="id" column="card_id" />
<result property="code" column="code" />
</association>了解了MyBatis中处理一对一关联关系的元素和方式后,下面就以个人和身份证之间的一对一关联关系为例,对MyBatis中一对一关联关系的处理进行详细讲解:
(1)创建数据表
在 heima_ssm_book 数据库中分别创建名称为 ch4_tb_idcard 的身份证数据表和名称为 ch4_tb_person 的个人数据表,同时预先插入两条数据,具体的SQL语句如下:
use heima_ssm_book;
create table ch4_tb_idcard
(
id int primary key auto_increment,
code varchar(18)
);
insert into ch4_tb_idcard(code)
values ('152221198711020624');
insert into ch4_tb_idcard(code)
values ('152201199008150317');
create table ch4_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 ch4_tb_idcard (id)
);
insert into ch4_tb_person(name, age, sex, card_id)
values ('Rose', 22, '女', 1);
insert into ch4_tb_person(name, age, sex, card_id)
values ('Jack', 23, '男', 2);完成上述操作后,ch4_tb_idcard 表和 ch4_tb_person 表中的数据如图所示。


(2)创建IdCard持久化类
在项目创建持久化类 src/main/java/io/weew12/github/pojo/IdCard.java,用于封装身份证信息。IdCard 类具体代码如文件4-1所示:
package io.weew12.github.pojo;
/**
* <p> ClassName: IdCard </p>
* <p> Package: io.weew12.github.pojo </p>
* <p> Description:
* 身份证持久化类
* </p>
*/
public class IdCard {
// 身份证id
private Integer id;
// 身份证号码
private String code;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
@Override
public String toString() {
return "IdCard{" +
"id=" + id +
", code='" + code + '\'' +
'}';
}
}在文件4-1中,分别定义了 IdCard 类的 id 和 code 属性,以及对应的 getter/setter 方法,同时提供了方便查看输出结果的 toString() 方法。
(3)创建Person持久化类
在项目下创建持久化类 src/main/java/io/weew12/github/pojo/Person.java,用于封装人员信息。Person 类具体代码如文件4-2所示:
package io.weew12.github.pojo;
/**
* <p> ClassName: Person </p>
* <p> Package: io.weew12.github.pojo </p>
* <p> Description:
* 人员信息
* </p>
*/
public class Person {
/**
* 人员id
*/
private Integer id;
/**
* 姓名
*/
private String name;
/**
* 年龄
*/
private Integer age;
/**
* 性别
*/
private String sex;
/**
* 人员关联的证件
*/
private IdCard card;
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 Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public IdCard getCard() {
return card;
}
public void setCard(IdCard card) {
this.card = card;
}
@Override
public String toString() {
return "Person{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
", sex='" + sex + '\'' +
", card=" + card +
'}';
}
}在文件4-2中,分别定义了 Person 类的人员id、姓名、年龄、性别和人员关联的证件等属性,以及属性对应的 getter/setter 方法,同时提供了方便查看输出结果的 toString() 方法。
(4)创建IdCardMapper.xml映射文件和IdCardMapper 接口文件
创建接口文件src/main/java/io/weew12/github/mapper/IdCardMapper.java:
package io.weew12.github.mapper;
import io.weew12.github.pojo.IdCard;
public interface IdCardMapper {
IdCard findCodeById(Integer id);
}在包中,创建身份证映射文件 src/main/resources/io/weew12/github/mapper/IdCardMapper.xml,并在映射文件中编写一对一关联映射查询的配置信息。IdCardMapper.xml 具体代码如文件4-3所示:
<?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="io.weew12.github.mapper.IdCardMapper">
<!-- 根据 id 查询证件信息 -->
<select id="findCodeById" parameterType="integer" resultType="idCard">
select *
from heima_ssm_book.ch4_tb_idcard
where id = #{id}
</select>
</mapper>(5)创建PersonMapper.xml映射文件和PersonMapper 接口文件
创建接口文件src/main/java/io/weew12/github/mapper/PersonMapper.java:
package io.weew12.github.mapper;
import io.weew12.github.pojo.Person;
public interface PersonMapper {
Person findPersonById(Integer id);
}在包中,创建人员映射文件 src/main/resources/io/weew12/github/mapper/PersonMapper.xml,并在映射文件中编写一对一关联映射查询的配置信息。PersonMapper.xml 具体代码如文件4-4所示:
<?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="io.weew12.github.mapper.PersonMapper">
<resultMap id="IdCardWithPersonResult" type="person">
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="age" column="age"/>
<result property="sex" column="sex"/>
<!-- 一对一:association 使用 select 属性引入另外一条 SQL 语句 -->
<association property="card" column="card_id" javaType="idCard"
select="io.weew12.github.mapper.IdCardMapper.findCodeById"/>
</resultMap>
<!-- 嵌套查询:通过执行另外一条 SQL 映射语句来返回预期的特殊类型 -->
<select id="findPersonById" parameterType="integer" resultMap="IdCardWithPersonResult">
select *
from heima_ssm_book.ch4_tb_person
where id = #{id}
</select>
</mapper>在文件4-4中,代码使用MyBatis中的嵌套查询方式进行人员及其关联的证件信息查询,<select> 元素 resultMap 属性的值需要与 <resultMap> 元素 id 属性的值相同。代码实现了人员类中的属性与数据库中字段的关联映射,因为返回的人员对象中除了基本属性外还有一个关联的 card 属性,所以需要使用 <association> 元素手动编写结果映射。
从映射文件 PersonMapper.xml 中可以看出,嵌套查询的方法是先执行一个简单的SQL语句,然后在进行结果映射时,在 <association> 元素中使用 select 属性执行另一条SQL语句(即 IdCardMapper.xml 中 id 为 findCodeById 的 select 查询语句)。
(6)配置核心配置文件
在核心配置文件 mybatis-config.xml 中,引入 IdCardMapper.xml 和 PersonMapper.xml 映射文件,并为 com.itheima.pojo 包下的所有实体类定义别名。mybatis-config.xml 的具体代码如文件4-5所示:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!--从外部properties文件中加载配置信息-->
<properties resource="db.properties"/>
<typeAliases>
<package name="io.weew12.github.pojo"/>
</typeAliases>
<!-- 数据库连接环境设置-->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${mysql.driver}"/>
<property name="url" value="${mysql.url}"/>
<property name="username" value="${mysql.username}"/>
<property name="password" value="${mysql.password}"/>
</dataSource>
</environment>
</environments>
<!-- 配置mapper xml路径-->
<mappers>
<package name="io/weew12/github/mapper"/>
</mappers>
</configuration>在文件4-5中,代码使用扫描包的形式为 io.weew12.github.pojo 包下的所有实体类定义别名;代码将io/weew12/github/mapper包内的映射器接口全部注册为映射器
(7)编写测试方法
为了验证上述配置,在测试类 MyBatisTest 中,编写测试方法 findPersonByIdTest(),具体代码如下:
/**
* + 查询人员及其关联的证件信息
*/
@Test
public void findPersonByIdTest() {
// 1.通过工具类获取 sqlSession 对象
SqlSession sqlSession = MybatisUtils.getSession();
// 2.使用MyBatis嵌套查询的方式查询id为1的人的信息
PersonMapper personMapper = sqlSession.getMapper(PersonMapper.class);
Person personById = personMapper.findPersonById(1);
// 3.输出查询结果信息
System.out.println(personById);
// 4.关闭 sqlSession
sqlSession.close();
}执行 MyBatisTest 测试类的 findPersonByIdTest() 方法,控制台的输出结果如图所示。

由图可知,使用MyBatis嵌套查询的方式查询出了id为1的人员及其关联的身份证信息,实现了MyBatis中的一对一关联查询。
**虽然使用嵌套查询的方式比较简单,但是MyBatis嵌套查询的方式要执行多条SQL语句,对于大型数据集合和列表展示来说,这样可能会导致成百上千条关联的SQL语句被执行,从而极大地消耗数据库性能并降低查询效率,这并不是开发人员所期望的。为此,可以使用MyBatis提供的嵌套结果方式进行关联查询。**下面修改上述案例,使用嵌套结果方式实现个人与身份证之间的关联关系查询:
(1)修改PersonMapper.xml映射文件
添加接口:
package io.weew12.github.mapper;
import io.weew12.github.pojo.Person;
public interface PersonMapper {
Person findPersonById(Integer id);
Person findPersonById2(Integer id);
}在 PersonMapper.xml 中,在 <mapper> 元素下添加使用MyBatis嵌套结果的方式进行人员及其关联的证件信息查询的代码:
<!-- 方式二 嵌套结果 -->
<select id="findPersonById2" parameterType="integer" resultMap="IdCardWithPersonResult2">
select p.*,
idcard.code
from heima_ssm_book.ch4_tb_person p,
heima_ssm_book.ch4_tb_idcard idcard
where p.card_id = idcard.id
and p.id = #{id}
</select>
<!-- 嵌套结果:使用嵌套结果映射来处理重复的联合结果的子集 -->
<resultMap id="IdCardWithPersonResult2" type="person">
<id property="id" column="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>从上述代码可以看出,MyBatis嵌套结果方式使用 <select> 元素编写了一条复杂的、多表关联的SQL语句(关联查询人员及其对应的身份证信息),并且在 <association> 元素中继续使用相关子元素进行数据库表字段和实体类属性的映射。这样做的好处是,无须在 IdCardMapper.xml 文件中编写与 PersonMapper.xml 文件相关联的SQL语句,在 PersonMapper.xml 文件中即可实现人员及其关联证件信息的查询。
(2)编写嵌套结果测试方法
在测试类 MyBatisTest 中编写测试方法 findPersonByIdTest2(),其代码如下:
/**
* + 查询人员及其关联的证件信息
*/
@Test
public void findPersonByIdTest2() {
// 1.通过工具类获取 sqlSession 对象
SqlSession sqlSession = MybatisUtils.getSession();
// 2.使用MyBatis嵌套查询的方式查询id为1的人的信息
PersonMapper personMapper = sqlSession.getMapper(PersonMapper.class);
Person personById = personMapper.findPersonById2(1);
// 3.输出查询结果信息
System.out.println(personById);
// 4.关闭 sqlSession
sqlSession.close();
}执行 MyBatisTest 测试类的 findPersonByIdTest2() 方法后,控制台的输出结果:

学一招:MyBatis延迟加载的配置
在使用MyBatis嵌套查询方式进行MyBatis关联映射查询时,使用MyBatis的延迟加载在一定程度上可以降低运行消耗并提高查询效率。MyBatis默认没有开启延迟加载,需要在核心配置文件 mybatis-config.xml 中的 <settings> 元素内进行配置。具体配置方式如下:
<settings>
<!-- 打开延迟加载的开关 -->
<setting name="lazyLoadingEnabled" value="true" />
<!-- 将积极加载改为按需加载 -->
<setting name="aggressiveLazyLoading" value="false" />
</settings>在映射文件中,MyBatis关联映射的 <association> 元素和 <collection> 元素中都已默认配置了延迟加载属性,即默认属性 fetchType="lazy"(fetchType="eager" 表示立即加载),所以在核心配置文件中开启延迟加载后,无须在映射文件中再做配置。
4.3 一对多查询
与一对一的关联关系相比,开发人员接触更多的关联关系是一对多(或多对一)。例如,一个用户可以有多个订单,多个订单也可以归一个用户所有。用户和订单的关联关系如图4-6所示。

图4-6 用户和订单的关联关系
在MyBatis中,通过 <collection> 元素来处理一对多关联关系。
<collection> 元素的属性大部分与 <association> 元素相同,但其还包含一个特殊属性——ofType。ofType 属性与 javaType 属性相对应,它用于指定实体类对象中集合类属性所包含的元素的类型。
与 <association> 元素一样,<collection> 元素也是 <resultMap> 元素的子元素,<collection> 元素也有嵌套查询和嵌套结果两种配置方式,具体如下:
1. 嵌套查询
<!--方式一:嵌套查询-->
<collection property="ordersList" column="id"
ofType="com.itheima.pojo.Orders"
select="com.itheima.mapper.OrdersMapper.selectOrdersByUserId" />2. 嵌套结果
<!--方式二:嵌套结果-->
<collection property="ordersList" ofType="com.itheima.pojo.Orders">
<id property="id" column="orders_id" />
<result property="number" column="number" />
</collection>在了解了MyBatis处理一对多关联关系的元素和方式后,下面以用户和订单之间的一对多关联关系为例,详细讲解如何在MyBatis中处理一对多关联关系:
(1)创建数据表
在名称为 heima_ssm_book的数据库中,创建两个数据表,分别为 ch4_tb_user(用户数据表)和 ch4_tb_orders(订单表),同时在表中预先插入几条测试数据。具体SQL语句如下:
use heima_ssm_book;
create table ch4_tb_user
(
id int(32) primary key auto_increment,
username varchar(32),
address varchar(256)
);
insert into ch4_tb_user
values (1, '小明', '北京');
insert into ch4_tb_user
values (2, '张华', '上海');
insert into ch4_tb_user
values (3, '李华', '上海');
create table ch4_tb_orders
(
id int(32) primary key auto_increment,
number varchar(32) not null,
user_id int(32) not null,
foreign key (user_id) references ch4_tb_user (id)
);
insert into ch4_tb_orders
values (1, '1000011', 1);
insert into ch4_tb_orders
values (2, '1000012', 2);
insert into ch4_tb_orders
values (3, '1000013', 3);执行完上述操作后,tb_user 表和 tb_orders 表中的数据如图4-7所示。


(2)创建Orders持久化类
在包中,创建持久化类 src/main/java/io/weew12/github/pojo/Orders.java,并在类中定义订单id和订单编号等属性。Orders 类具体代码如文件4-6所示:
package io.weew12.github.pojo;
public class Orders {
/**
* 订单id
*/
private Integer id;
/**
* 订单编号
*/
private String number;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
@Override
public String toString() {
return "Orders{" +
"id=" + id +
", number='" + number + '\'' +
'}';
}
}在文件4-6中,Orders 类定义了订单的属性和对应的 getter/setter 方法,同时为了方便查看输出结果,重写了 toString() 方法。
(3)创建Users持久化类
在包中,创建持久化类 src/main/java/io/weew12/github/pojo/Users.java,并在类中定义用户编号、用户姓名、用户地址和用户关联的订单等属性。Users 类具体代码如文件4-7所示:
package io.weew12.github.pojo;
import java.util.List;
public class Users {
/**
* 用户id
*/
private Integer id;
/**
* 用户姓名
*/
private String username;
/**
* 用户地址
*/
private String address;
/**
* 用户关联的订单
*/
private List<Orders> ordersList;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public List<Orders> getOrdersList() {
return ordersList;
}
public void setOrdersList(List<Orders> ordersList) {
this.ordersList = ordersList;
}
@Override
public String toString() {
return "Users{" +
"id=" + id +
", username='" + username + '\'' +
", address='" + address + '\'' +
", ordersList=" + ordersList +
'}';
}
}在文件4-7中,Users 类定义了用户属性和对应的 getter/setter 方法,同时为了方便查看输出结果,重写了 toString() 方法。
(4)创建UsersMapper.xml映射文件和UsersMapper 接口文件
接口文件:
package io.weew12.github.mapper;
import io.weew12.github.pojo.Orders;
import java.util.List;
public interface UsersMapper {
List<Orders> findUserWithOrders(Integer id);
}在包中,创建用户实体映射文件src/main/resources/io/weew12/github/mapper/UsersMapper.xml,并在文件中编写一对多关联映射查询的配置。UsersMapper.xml 具体代码如文件4-8所示:
<?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="io.weew12.github.mapper.UsersMapper">
<!-- 一对多:查看某一用户及其关联的订单信息
注意:当关联查询出的列名相同时,则需要使用别名区分 -->
<select id="findUserWithOrders" parameterType="integer" resultMap="UserWithOrdersResult">
select u.*, o.id as orders_id, o.number
from heima_ssm_book.ch4_tb_user u,
heima_ssm_book.ch4_tb_orders o
where u.id = o.user_id
and u.id = #{id}
</select>
<resultMap id="UserWithOrdersResult" type="users">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="address" column="address"/>
<!-- 一对多关联映射:collection
ofType 表示属性集合中元素的类型, List<Orders>属性即 Orders 类 -->
<collection property="ordersList" ofType="orders">
<id property="id" column="orders_id"/>
<result property="number" column="number"/>
</collection>
</resultMap>
</mapper>在文件中,使用MyBatis嵌套结果的方式定义了一个根据用户id查询用户及其关联的订单信息的select语句。因为返回的用户对象中包含 Orders 集合对象属性,所以需要手动编写结果映射信息。其中,写了一条复杂的、多表关联的SQL语句(关联查询用户及其对应的订单信息);代码在元素中使用相关子元素进行数据库表字段和实体类属性的映射。
(5)配置核心配置文件
同 4.2
(6)编写测试方法
在测试类 MyBatisTest 中编写测试方法 findUserTest(),其代码如下:
/**
* + 一对多查询
*/
@Test
public void findUserTest() {
// 1.通过工具类获取 sqlSession 对象
SqlSession sqlSession = MybatisUtils.getSession();
// 2.使用MyBatis嵌套查询的方式查询id为1的人的信息
UsersMapper usersMapper = sqlSession.getMapper(UsersMapper.class);
List<Orders> userWithOrders = usersMapper.findUserWithOrders(1);
// 3.输出查询结果信息
System.out.println(userWithOrders);
// 4.关闭 sqlSession
sqlSession.close();
}
由图可知,使用MyBatis嵌套结果的方式查询出了id为1的用户及其关联的订单集合信息,说明程序实现了一个用户对多个订单的一对多关联查询。
需要注意的是,上述案例从用户的角度出发,用户与订单之间是一对多的关联关系,但如果从单个订单的角度出发,一个订单只能属于一个用户,即一对一的关联关系。读者可根据4.2节内容实现单个订单与用户之间的一对一关联关系查询,这里不再赘述。
4.4 多对多查询
在实际项目开发中,多对多的关联关系也是非常常见的。以订单和商品为例,一个订单可以包含多种商品,而一种商品又可以属于多个订单,订单和商品就属于多对多的关联关系,订单和商品之间的关联关系如图4-9所示。
在数据库中,多对多的关联关系通常使用一个中间表来维护,中间表中的订单id作为外键关联订单表的id,中间表中的商品id作为外键关联商品表的id。这3个表之间的关系如图4-10所示。

图4-9 订单和商品之间的关联关系

图4-10 数据库中订单表、中间表与商品表之间的关系
了解了数据库中订单表与商品表之间的多对多关联关系后,下面以订单表与商品表之间的多对多关系为例来讲解如何使用MyBatis处理多对多的关系:
(1)创建数据表
在名称为 heima_ssm_book 的数据库中,创建名称为 ch4_tb_product 的商品表和名称为 ch4_tb_orders_item 的中间表,同时在表中预先插入几条数据。具体的SQL语句如下:
use heima_ssm_book;
create table ch4_tb_product
(
id int(32) primary key auto_increment,
name varchar(32),
price double
);
insert into ch4_tb_product
values (1, 'Java 基础入门', 45.0);
insert into ch4_tb_product
values (2, 'Java Web 程序开发入门', 138.5);
insert into ch4_tb_product
values (3, 'SSM 框架整合实战', 150.0);
create table ch4_tb_orders_item
(
id int(32) primary key auto_increment,
orders_id int(32),
product_id int(32),
foreign key (orders_id) references ch4_tb_orders (id),
foreign key (product_id) references ch4_tb_product (id)
);
insert into ch4_tb_orders_item
values (1, 1, 1);
insert into ch4_tb_orders_item
values (2, 2, 2);
insert into ch4_tb_orders_item
values (3, 3, 3);
insert into ch4_tb_orders_item
values (4, 1, 2);由于订单表在4.3节中已经创建,所以这里只创建了商品表和中间表。完成上述操作后,ch4_tb_product 表和 ch4_tb_orders_item 表中的数据如图4-11所示。


(2)创建Product持久化类
在 io.weew12.github.pojo 包中,创建持久化类 Product,并在类中定义商品id、商品名称、商品单价等属性。Product 类具体代码如文件4-9所示:
package io.weew12.github.pojo;
public class Product {
/**
* 商品id
*/
private Integer id;
/**
* 商品名称
*/
private String name;
/**
* 商品单价
*/
private Double price;
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 Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
@Override
public String toString() {
return "Product{" +
"id=" + id +
", name='" + name + '\'' +
", price=" + price +
'}';
}
}(3)修改Orders持久化类
在 Orders 持久化类中添加商品集合属性,用于存储订单关联的商品信息:
package io.weew12.github.pojo;
import java.util.List;
public class Orders {
/**
* 订单id
*/
private Integer id;
/**
* 订单编号
*/
private String number;
/**
* 订单关联的商品
*/
private List<Product> productList;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
public List<Product> getProductList() {
return productList;
}
public void setProductList(List<Product> productList) {
this.productList = productList;
}
@Override
public String toString() {
return "Orders{" +
"id=" + id +
", number='" + number + '\'' +
", productList=" + productList +
'}';
}
}(4)创建ProductMapper.xml映射文件和ProductMapper 接口
创建接口:
package io.weew12.github.mapper;
import io.weew12.github.pojo.Product;
import java.util.List;
public interface ProductMapper {
List<Product> findProductById(Integer id);
}在 src/main/resources/io/weew12/github/mapper 包中,创建商品映射文件 ProductMapper.xml,并在文件中编写根据订单id查询商品信息的SQL语句:
<?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="io.weew12.github.mapper.ProductMapper">
<select id="findProductById" parameterType="integer" resultType="product">
select *
from heima_ssm_book.ch4_tb_product
where id in (select product_id
from heima_ssm_book.ch4_tb_orders_item
where orders_id = #{id})
</select>
</mapper>在文件中,定义了一个id为 findProductById 的执行语句,该执行语句中的SQL会根据订单id查询与该订单关联的商品信息。由于订单和商品是多对多的关联关系,所以需要通过中间表来查询商品信息。
(5)创建OrdersMapper.xml映射文件和OrdersMapper 接口
创建接口:
package io.weew12.github.mapper;
import io.weew12.github.pojo.Orders;
import java.util.List;
public interface OrdersMapper {
List<Orders> findOrdersWithProduct(Integer id);
List<Orders> findOrdersWithProduct2(Integer id);
}在包中,创建订单映射文件 src/main/resources/io/weew12/github/mapper/OrdersMapper.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="io.weew12.github.mapper.OrdersMapper">
<!-- 多对多嵌套查询:查询某订单及其关联的商品详情 -->
<select id="findOrdersWithProduct" parameterType="integer" resultMap="OrdersWithProductResult">
select *
from heima_ssm_book.ch4_tb_orders
where id = #{id}
</select>
<resultMap id="OrdersWithProductResult" type="orders">
<id property="id" column="id"/>
<result property="number" column="number"/>
<!-- 多对多关联映射:collection -->
<collection property="productList" ofType="product"
select="io.weew12.github.mapper.ProductMapper.findProductById" column="id"/>
</resultMap>
</mapper>(6)配置核心配置文件
同上4.2
(7)编写测试方法
在测试类 MyBatisTest 中编写多对多关联查询的测试方法 findOrdersTest(),其代码如下:
/**
* + 多对多查询
*/
@Test
public void findOrdersTest() {
// 1.通过工具类获取 sqlSession 对象
SqlSession sqlSession = MybatisUtils.getSession();
// 2.查询 id 为 1 的订单中的商品信息
OrdersMapper ordersMapper = sqlSession.getMapper(OrdersMapper.class);
List<Orders> ordersWithProduct = ordersMapper.findOrdersWithProduct(1);
// 3.输出查询结果信息
ordersWithProduct.forEach(System.out::println);
// 4.关闭 sqlSession
sqlSession.close();
}执行 MyBatisTest 测试类的 findOrdersTest() 方法,控制台的输出结果如图所示。

由图可知,使用MyBatis嵌套查询的方式查询出了一条id为1的订单信息及其关联的商品信息,说明实现了订单对多个商品的多对多关联查询。
除了使用嵌套查询的方式查询订单及其关联的商品信息外,还可以在 OrdersMapper.xml 中使用嵌套结果的方式进行查询:
/**
* + 多对多查询
*/
@Test
public void findOrdersTest2() {
// 1.通过工具类获取 sqlSession 对象
SqlSession sqlSession = MybatisUtils.getSession();
// 2.查询 id 为 1 的订单中的商品信息
OrdersMapper ordersMapper = sqlSession.getMapper(OrdersMapper.class);
List<Orders> ordersWithProduct = ordersMapper.findOrdersWithProduct2(1);
// 3.输出查询结果信息
ordersWithProduct.forEach(System.out::println);
// 4.关闭 sqlSession
sqlSession.close();
}
MyBatis嵌套结果的方式只编写了一条复杂的、多表关联的SQL语句,用于查询订单及其关联的商品信息,然后在 <collection> 元素中使用相关子元素进行数据表字段和实体类属性的映射。
4.5 MyBatis缓存机制
在实际项目开发中,通常对数据库查询的性能要求很高,MyBatis中通过缓存机制来减轻数据库压力,提高数据库性能。MyBatis的查询缓存分为一级缓存和二级缓存。
4.5.1 一级缓存
MyBatis的一级缓存是 **SqlSession** 级别的缓存。如果同一个 SqlSession 对象多次执行完全相同的SQL语句,在第一次执行完成后,MyBatis会将查询结果写入到一级缓存中,此后,如果程序没有执行插入、更新、删除操作,当第二次执行相同的查询语句时,MyBatis会直接读取一级缓存中的数据,而不用再去数据库查询,从而提高了数据库的查询效率。
例如,从数据表 ch4_tb_book 中多次查询id为1的图书信息,当程序第一次查询id为1的图书信息时,程序会将查询结果写入MyBatis一级缓存,当程序第二次查询id为1的图书信息时,MyBatis直接从一级缓存中读取,不再访问数据库进行查询。当程序对数据库执行了插入、更新、删除操作,MyBatis会清空一级缓存中的内容以防止程序误读。查询的具体过程如图4-13所示。

图4-13 查询id为1的图书信息
下面通过一个案例来对MyBatis一级缓存的应用进行详细讲解,该案例要求根据图书id查询图书信息:
① 创建数据表
在 heima_ssm_book 数据库中创建一个名称为 ch4_tb_book 的数据表,同时预先插入几条测试数据。具体的SQL语句如下:
use heima_ssm_book;
create table ch4_tb_book
(
id int primary key auto_increment,
book_name varchar(255),
price double,
author varchar(40)
);
insert into ch4_tb_book(book_name, price, author)
values ('Java 基础入门', 45.0, '传智播客高教产品研发部');
insert into ch4_tb_book(book_name, price, author)
values ('Java 基础案例教程', 48.0, '黑马程序员');
insert into ch4_tb_book(book_name, price, author)
values ('JavaWeb 程序设计任务教程', 50.0, '黑马程序员');完成上述操作后,数据库 ch4_tb_book 表中的数据如图所示。

② 创建Book持久化类
在项目包下创建持久化类 Book,在 Book 类中定义图书id、图书名称、价格、作者等属性,以及属性对应的getter/setter方法。Book 类具体代码如文件4-12所示:
package io.weew12.github.pojo;
public class Book {
/**
* 图书id
*/
private Integer id;
/**
* 图书名称
*/
private String bookName;
/**
* 价格
*/
private double price;
/**
* 作者
*/
private String author;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getBookName() {
return bookName;
}
public void setBookName(String bookName) {
this.bookName = bookName;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
@Override
public String toString() {
return "Book{" +
"id=" + id +
", bookName='" + bookName + '\'' +
", price=" + price +
", author='" + author + '\'' +
'}';
}
}③ 创建图书映射文件BookMapper.xml 和BookMapper 接口
接口:
package io.weew12.github.mapper;
import io.weew12.github.pojo.Book;
public interface BookMapper {
Book findBookById(Integer id);
int updateBook(Book book);
}在包中,创建图书映射文件 src/main/resources/io/weew12/github/mapper/BookMapper.xml,并在该文件中编写根据图书id查询图书信息的SQL语句。BookMapper.xml 具体代码如文件4-13所示:
<?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="io.weew12.github.mapper.BookMapper">
<!-- 根据 id 查询图书信息-->
<resultMap id="BookMap" type="book">
<id property="id" column="id"/>
<result property="bookName" column="book_name"/>
<result property="author" column="author"/>
<result property="price" column="price"/>
</resultMap>
<select id="findBookById" parameterType="integer" resultMap="BookMap">
select *
from heima_ssm_book.ch4_tb_book
where id = #{id}
</select>
<!-- 根据 id 更新图书信息-->
<update id="updateBook" parameterType="book">
update heima_ssm_book.ch4_tb_book
set book_name = #{bookName},
price=#{price}
where id = #{id}
</update>
</mapper>在文件4-13中,代码定义了一个 select 查询语句,可根据id查询对应的图书信息;定义了一个更新语句,可根据id更新对应的图书信息。
④ 配置核心配置文件
同4.2
⑤ 添加log4j2 依赖
由于需要通过log4j2 日志组件查看一级缓存的工作状态,所以需要在 pom.xml 中引入log4j2 的相关依赖。具体代码如下:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
......
<dependencies>
......
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.24.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.24.1</version>
</dependency>
</dependencies>
</project>⑥ 配置log4j2 日志组件
在 src/main/resources 目录下创建 log4j2.xml文件,用于配置MyBatis和控制台的日志。log4j2.xml具体代码如文件4-14所示:
设置日志输出到控制台
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
<Appenders>
<Console name="console" target="SYSTEM_OUT">
<PatternLayout pattern="%d [%-5p] %c - %m%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="ALL">
<AppenderRef ref="console"/>
</Root>
<Logger name="java.sql.ResultSet" additivity="false">
<AppenderRef level="INFO" ref="console"/>
</Logger>
<Logger name="org.apache" additivity="false">
<AppenderRef level="INFO" ref="console"/>
</Logger>
<Logger name="java.sql.Connection" additivity="false">
<AppenderRef level="DEBUG" ref="console"/>
</Logger>
<Logger name="java.sql.Statement" additivity="false">
<AppenderRef level="DEBUG" ref="console"/>
</Logger>
<Logger name="java.sql.PreparedStatement" additivity="false">
<AppenderRef level="DEBUG" ref="console"/>
</Logger>
</Loggers>
</Configuration>ref:https://logging.apache.org/log4j/2.x/manual/configuration.html
⑦ 编写测试方法findBookByIdTest1()
为了验证上述配置,在测试类 MyBatisTest 中编写测试方法 findBookByIdTest1(),具体代码如下:
/**
* + 根据 id 查询图书信息
*/
@Test
public void findBookByIdTest1() {
// 1. 通过工具类获取 sqlSession 对象
SqlSession sqlSession = MybatisUtils.getSession();
// 2. 使用 session 查询 id 为 1 的图书的信息
BookMapper bookMapper = sqlSession.getMapper(BookMapper.class);
Book bookById1 = bookMapper.findBookById(1);
// 3. 输出查询结果信息
System.out.println(bookById1);
// 4. 使用 session 再次查询 id 为 1 的图书的信息
Book bookById2 = bookMapper.findBookById(1);
// 5. 输出查询结果信息
System.out.println(bookById2);
// 6. 关闭 SqlSession
sqlSession.close();
}
由图可知,控制台输出了执行的SQL语句日志信息和查询结果。通过分析SQL语句日志信息可以发现,当程序第一次查询id为1的图书信息时,程序向数据库发送了SQL语句,当程序再次执行相同的查询语句时,程序没有再向数据库发送SQL语句进行查询,但依然得到了要查询的信息,这是因为程序直接从一级缓存中获取到了要查询的数据。
当程序对数据库执行了插入、更新、删除操作后,MyBatis会清空一级缓存中的内容,以防止程序误读。MyBatis一级缓存被清空之后,再次使用SQL查询语句访问数据库时,MyBatis会重新访问数据库。
例如,首先查询id为1的图书信息,然后使用更新语句对数据库中的图书信息进行更改,更改之后,再次对id为1的图书信息进行查询时,MyBatis依然会从数据库中查询。具体过程如下:
编写测试方法findBookByIdTest2()
在测试类 MyBatisTest 中编写测试方法 findBookByIdTest2() 进行测试,具体代码如下:
/**
* + 测试更新操作对一级缓存的影响
*/
@Test
public void findBookByIdTest2() {
// 1. 通过工具类生成 SqlSession 对象
SqlSession sqlSession = MybatisUtils.getSession();
// 2. 使用 session 查询 id 为 1 的图书的信息
BookMapper bookMapper = sqlSession.getMapper(BookMapper.class);
Book bookById1 = bookMapper.findBookById(1);
// 3. 输出查询结果信息
System.out.println(bookById1);
// 4. 创建一个新的 Book 对象并设置属性
Book book = new Book();
book.setId(1);
book.setBookName("MySQL 数据库入门");
book.setPrice(40.0);
// 5. 使用 session 更新 id 为 1 的图书的信息
int i = bookMapper.updateBook(book);
sqlSession.commit();
// 6. 再次查询 id 为 1 的图书的信息
Book bookById2 = bookMapper.findBookById(1);
// 7. 输出查询结果信息
System.out.println(bookById2);
// 8. 关闭 SqlSession
sqlSession.close();
}执行 MyBatisTest 测试类的 findBookByIdTest2() 方法,控制台的输出结果如图4-16所示:

由图可知,控制台输出了执行SQL语句的日志信息和查询结果。通过分析SQL语句日志信息可以发现,当程序第一次查询id为1的图书信息时,程序向数据库发送了SQL语句,然后使用update语句执行了更新操作。数据库更新之后,程序再次执行查询语句,程序就会向数据库发送SQL语句。由此可见,MyBatis的一级缓存在执行更新语句后被清空了。
4.5.2 二级缓存
相同的Mapper类使用相同的SQL语句,如果 **SqlSession** 不同,则两个 **SqlSession** 查询数据库时,会查询数据库两次,这样也会降低数据库的查询效率。
测试:
/**
* 不同的sqlSession调用同一个Mapper的查询方法
*/
@Test
public void findBookByIdTest3() {
// 创建两个不同的sqlSession
SqlSession sqlSession1 = MybatisUtils.getSession();
SqlSession sqlSession2 = MybatisUtils.getSession();
// 基于sqlSession1查询
BookMapper bookMapper1 = sqlSession1.getMapper(BookMapper.class);
Book bookById1 = bookMapper1.findBookById(1);
System.out.println(bookMapper1);
// 基于sqlSession2查询
BookMapper bookMapper2 = sqlSession2.getMapper(BookMapper.class);
Book bookById2 = bookMapper2.findBookById(1);
System.out.println(bookById2);
sqlSession1.close();
sqlSession2.close();
}
为了解决这个问题,就需要用到MyBatis的二级缓存。MyBatis的二级缓存是**Mapper**级别的缓存,与一级缓存相比,二级缓存的范围更大,多个 **SqlSession** 可以共用二级缓存,并且二级缓存可以自定义缓存资源。
在MyBatis中,一个 Mapper.xml 文件通常被称为一个Mapper,MyBatis以namespace区分Mapper,如果多个 **SqlSession** 对象使用同一个**Mapper**的相同查询语句去操作数据库,在第一个 **SqlSession** 对象执行完后,MyBatis会将查询结果写入二级缓存,此后,如果程序没有执行插入、更新、删除操作,当第二个 **SqlSession** 对象执行相同的查询语句时,MyBatis会直接读取二级缓存中的数据。MyBatis二级缓存的执行过程如图4-17所示:

图4-17 MyBatis 二级缓存的执行过程
与MyBatis的一级缓存不同的是,MyBatis的二级缓存需要手动开启,开启二级缓存通常要完成以下两个步骤:
① 开启二级缓存的全局配置
使用二级缓存前,需要在MyBatis的核心配置 mybatis-config.xml 文件中通过 <settings> 元素开启二级缓存的全局配置,具体代码如下:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!--从外部properties文件中加载配置信息-->
<properties resource="db.properties"/>
<!-- 全局设置-->
<settings>
<!-- 开启二级缓存-->
<setting name="cacheEnabled" value="true"/>
</settings>
......
</configuration>在上述代码中,cacheEnabled 的value值为true,表示在此配置文件下开启MyBatis的二级缓存。
② 开启当前Mapper的namespace下的二级缓存
开启当前Mapper的namespace下的二级缓存,可以通过MyBatis映射文件中的 <cache> 元素来完成,在 <mapper> 元素下添加的代码如下:
<?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="io.weew12.github.mapper.BookMapper">
<!-- 开启当前 Mapper 的 namespace 下的二级缓存-->
<cache/>
<!-- 根据 id 查询图书信息-->
<resultMap id="BookMap" type="book">
<id property="id" column="id"/>
<result property="bookName" column="book_name"/>
<result property="author" column="author"/>
<result property="price" column="price"/>
</resultMap>
<select id="findBookById" parameterType="integer" resultMap="BookMap">
select *
from heima_ssm_book.ch4_tb_book
where id = #{id}
</select>
<!-- 根据 id 更新图书信息-->
<update id="updateBook" parameterType="book">
update heima_ssm_book.ch4_tb_book
set book_name = #{bookName},
price=#{price}
where id = #{id}
</update>
</mapper>以上代码开启了当前Mapper的namespace下的二级缓存,此时二级缓存处于默认状态,默认状态的二级缓存可以实现的功能如下:
- 映射文件中所有
select语句将会被缓存。 - 映射文件中的所有
insert、update和delete语句都会刷新缓存。 - 缓存会使用
LRU算法回收。 - 没有刷新间隔,缓存不会以任何时间顺序来刷新。
- 缓存会存储列表集合或对象的
1024个引用。 - 缓存是可读/可写的缓存,这意味着对象检索不是共享的,缓存可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。
以上是二级缓存在默认状态下的特性,如果需要调整上述特性,可通过 <cache> 元素的属性来实现,<cache> 元素的属性具体如表4-2所示:
表4-2 **<cache>** 元素的属性
| 属性 | 说明 |
|---|---|
flushInterval | 刷新间隔。该属性可以被设置为任意的正整数,代表一个合理的毫秒形式的时间段。默认情况下是不设置值,即没有刷新间隔,只在调用语句时刷新 |
size | 引用数目。该属性可以被设置为任意正整数,要记住缓存的对象数目和运行环境的可用内存资源数目,默认值为1024 |
readOnly | 只读。该属性可以被设置为true或者false。当缓存设置为只读时,缓存对象不能被修改,但此时缓存性能较高。当缓存设置为可读写时,性能较低,但安全性高 |
eviction | 收回策略。该属性有4个可选值,具体如下: • LRU:最近最少使用的策略,移除最长时间不被使用的对象 • FIFO:先进先出策略,按对象进入缓存的顺序来移除它们 • SOFT:软引用策略,移除基于垃圾回收器状态和软引用规则的对象 • WEAK:弱引用策略,更积极地移除基于垃圾收集器状态和弱引用规则的对象 |
在讲解了MyBatis二级缓存的执行过程及如何开启MyBatis二级缓存后,下面通过一个案例来演示MyBatis二级缓存的应用,该案例依然根据id查询图书信息:
修改 Book 实体类,实现序列化接口,缓存要用到,否则无法缓存:
package io.weew12.github.pojo;
import java.io.Serializable;
public class Book implements Serializable {
......
}编写测试方法findBookByIdTest4()
为了验证上述配置,在测试类 MyBatisTest 中编写测试方法 findBookByIdTest4(),具体代码如下:
@Test
public void findBookByIdTest4() {
// 创建两个不同的sqlSession
SqlSession sqlSession1 = MybatisUtils.getSession();
SqlSession sqlSession2 = MybatisUtils.getSession();
// 基于sqlSession1查询
BookMapper bookMapper1 = sqlSession1.getMapper(BookMapper.class);
Book bookById1 = bookMapper1.findBookById(1);
System.out.println(bookMapper1);
sqlSession1.close();
// 基于sqlSession2查询
BookMapper bookMapper2 = sqlSession2.getMapper(BookMapper.class);
Book bookById2 = bookMapper2.findBookById(1);
System.out.println(bookById2);
sqlSession2.close();
}在上述代码中,代码通过 MyBatisUtils 工具类获取两个 SqlSession 对象;通过第一个 SqlSession 对象 session1 调用 Mapper 对象的方法获取id为1的图书信息,然后对查询结果进行输出,最后关闭 SqlSession;通过第二个 SqlSession 对象 session2 调用 Mapper 对象的方法再次获取id为1的图书信息,然后对查询结果进行输出,最后关闭 SqlSession。
执行 MyBatisTest 测试类的 findBookByIdTest4() 方法,控制台的输出结果如图所示。

由图可知,控制台输出了执行SQL语句的日志信息和查询结果。通过分析SQL语句日志信息可以发现,当第一个 SqlSession 对象 session1 执行查询时,CacheHitRatio(缓存命中率)为0,程序发送了SQL语句;当第二个 SqlSession 对象 session2 执行相同的查询时,CacheHitRatio为0.5,程序没有发出SQL语句,这就说明程序直接从二级缓存中获取了数据。
注意点:
- 二级缓存在
**SqlSession**关闭或提交之后有效 - 只有在配置了 mapper 的缓存配置和全局的二级缓存配置之后,当某个 sqlSession 关闭或提交的时候,才会把数据刷入二级缓存
重新执行开启二级缓存后的测试样例findBookByIdTest3(注意两个 sqlSession 的关闭时机和findBookByIdTest4的区别是不一样的):
@Test
public void findBookByIdTest3() {
// 创建两个不同的sqlSession
SqlSession sqlSession1 = MybatisUtils.getSession();
SqlSession sqlSession2 = MybatisUtils.getSession();
// 基于sqlSession1查询
BookMapper bookMapper1 = sqlSession1.getMapper(BookMapper.class);
Book bookById1 = bookMapper1.findBookById(1);
System.out.println(bookMapper1);
// 基于sqlSession2查询
BookMapper bookMapper2 = sqlSession2.getMapper(BookMapper.class);
Book bookById2 = bookMapper2.findBookById(1);
System.out.println(bookById2);
sqlSession1.close();
sqlSession2.close();
}
从输出可以发现,第二次 sqlSession2 的查询并没有命中缓存,因为第二次查询的时候第一个 sqlSession 还没有关闭,所以他的一级缓存数据还没有被写入二级缓存中,所以 sqlSession2 没有办法复用二级缓存数据。
在实际开发中,经常会遇到多个 SqlSession 在同一个Mapper中执行操作的情况,例如,SqlSession1 执行查询操作,SqlSession2 执行插入、更新、删除操作,SqlSession3 又执行与 SqlSession1 相同的查询操作。当 SqlSession1 执行查询操作时,程序会将查询结果写入MyBatis二级缓存;当 SqlSession2 对数据库执行了插入、更新、删除操作后,MyBatis会清空二级缓存中的内容,以防止程序误读,当 SqlSession3 执行与 SqlSession1 相同的查询操作时,MyBatis会重新访问数据库。
③ 编写测试方法findBookByIdTest5()
下面编写一个测试方法来演示上述二级缓存的清空。在测试类 MyBatisTest 中,编写测试方法 findBookByIdTest5(),具体代码如下:
/**
* + 测试更新操作对二级缓存的影响
*/
@Test
public void findBookByIdTest5() {
// 1. 通过工具类生成 SqlSession 对象
SqlSession session1 = MybatisUtils.getSession();
SqlSession session2 = MybatisUtils.getSession();
SqlSession session3 = MybatisUtils.getSession();
// 2. 使用 session1 查询 id 为 1 的图书信息
BookMapper mapper1 = session1.getMapper(BookMapper.class);
Book bookById1 = mapper1.findBookById(1);
// 3. 输出查询结果信息
System.out.println(bookById1);
// 4. 关闭 SqlSession1
session1.close();
// 5. 创建一个新的 Book 对象并设置属性
Book book = new Book();
book.setId(2);
book.setBookName("Java Web 程序开发进阶");
book.setPrice(45.0);
// 6. 使用 session2 更新 id 为 2 的图书信息
BookMapper mapper2 = session2.getMapper(BookMapper.class);
int i = mapper2.updateBook(book);
session2.commit();
session2.close();
// 7. 使用 session3 查询 id 为 1 的图书信息
BookMapper mapper3 = session3.getMapper(BookMapper.class);
Book bookById = mapper3.findBookById(1);
// 8. 输出查询结果信息
System.out.println(bookById);
// 9. 关闭 SqlSession3
session3.close();
}在上述代码中,代码通过 MyBatisUtils 工具类获取3个 SqlSession 对象;首先通过第1个 SqlSession 对象 session1 调用查询id为1的图书信息,然后对查询结果进行输出,最后关闭 SqlSession;使用第2个 SqlSession 对象 session2 调用方法更新id为2的图书的信息;通过第3个 SqlSession 对象 session3 调用方法再次获取id为1的图书信息,然后对查询结果进行输出,最后关闭 SqlSession。
执行 MyBatisTest 测试类的 findBookByIdTest5() 方法,控制台的输出结果如图所示:

由图可知,控制台输出了执行SQL语句的日志信息和查询结果。通过分析SQL语句日志信息可以发现,当第1个 SqlSession 对象 session1 执行查询操作时,Cache Hit Ratio(缓存命中率)为0,程序发送了SQL语句给数据库;在第2个 SqlSession 对象 session2 对数据库执行更新操作后,第3个 SqlSession 对象 session3 执行与session1相同的查询,Cache HitRatio(缓存命中率)也为0,程序也发送SQL语句给数据库。这就说明,程序在执行更新操作后,MyBatis会清空二级缓存中的数据,再次执行查询操作时,程序依然会从数据库进行查询。
多学一招:Cache Hit Ratio
终端用户访问缓存时,如果在缓存中查找到了要被访问的数据,就称为命中。如果缓存中没有查找到要被访问的数据,就是没有命中。当多次执行查询操作时,缓存命中次数与总的查询次数(缓存命中次数+缓存没有命中次数)的比,就称为缓存命中率,即缓存命中率=缓存命中次数/总的查询次数。当MyBatis开启二级缓存后,第一次查询数据时,由于数据还没有进入缓存,所以需要在数据库中查询而不是在缓存中查询,此时,缓存命中率为0。第一次查询过后,MyBatis会将查询到的数据写入缓存中,当第二次再查询相同的数据时,MyBatis会直接从缓存中获取这条数据,缓存将命中,此时的缓存命中率为0.5(1/2)。当第三次查询相同的数据,则缓存命中率为0.66666(2/3),以此类推。
4.6 案例:商品的类别
现有一个商品表 ch4_product 和一个商品类别表 ch4_category,其中,商品类别表 ch4_category 和商品表 ch4_product 是一对多的关系。商品表 ch4_product 和商品类别表 ch4_category 分别如表 4-3 和表 4-4 所示。


本案例具体要求如下:根据表 4-3 和表 4-4 在数据库中分别创建一个商品表 product 和一个商品类别表 category,并通过 MyBatis 查询商品类别为“白色家电”的商品的所有信息。
创建数据表:
use heima_ssm_book;
create table ch4_category
(
id int(10) primary key auto_increment,
type_name varchar(30)
);
insert into ch4_category
values (1, '黑色家电'),
(2, '白色家电');
create table ch4_product
(
id int(10) primary key auto_increment,
goods_name varchar(30),
price double,
type_id int(10),
foreign key (type_id) references ch4_category (id)
);
insert into ch4_product
values (1, '电视机', 5000.00, 1),
(2, '冰箱', 4000.00, 2),
(3, '空调', 3000.00, 2),
(4, '洗衣机', 2000.00, 2);创建实体类:
package io.weew12.github.pojo;
import java.util.List;
public class Category {
/**
* 商品类别编号
*/
private Integer id;
/**
* 商品类别名称
*/
private String typeName;
/**
* 具体商品
*/
private List<Product2> product2List;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getTypeName() {
return typeName;
}
public void setTypeName(String typeName) {
this.typeName = typeName;
}
public List<Product2> getProduct2List() {
return product2List;
}
public void setProduct2List(List<Product2> product2List) {
this.product2List = product2List;
}
@Override
public String toString() {
return "Category{" +
"id=" + id +
", typeName='" + typeName + '\'' +
", product2List=" + product2List +
'}';
}
}package io.weew12.github.pojo;
public class Product2 {
/**
* 商品编号
*/
private Integer id;
/**
* 商品名称
*/
private String goodsName;
/**
* 商品单价
*/
private double price;
/**
* 商品类别
*/
private Integer typeId;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getGoodsName() {
return goodsName;
}
public void setGoodsName(String goodsName) {
this.goodsName = goodsName;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
public Integer getTypeId() {
return typeId;
}
public void setTypeId(Integer typeId) {
this.typeId = typeId;
}
@Override
public String toString() {
return "Product2{" +
"id=" + id +
", goodsName='" + goodsName + '\'' +
", price=" + price +
", typeId=" + typeId +
'}';
}
}嵌套查询
创建 mapper 接口和 xml:
package io.weew12.github.mapper;
import io.weew12.github.pojo.Category;
import java.util.List;
public interface CategoryMapper {
List<Category> findWithProduct2(String typeName);
}<?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="io.weew12.github.mapper.CategoryMapper">
<resultMap id="CategoryWithProduct2" type="cateGory">
<id property="id" column="id"/>
<result property="typeName" column="type_name"/>
<collection property="product2List" ofType="product2"
select="io.weew12.github.mapper.Product2Mapper.findByTypeId" column="id"/>
</resultMap>
<select id="findWithProduct2" parameterType="string" resultMap="CategoryWithProduct2">
select *
from heima_ssm_book.ch4_category
where type_name = #{typeName}
</select>
</mapper>package io.weew12.github.mapper;
import io.weew12.github.pojo.Product2;
public interface Product2Mapper {
Product2 findByTypeId(Integer typeId);
}<?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="io.weew12.github.mapper.Product2Mapper">
<resultMap id="Product2Map" type="product2">
<id property="id" column="id"/>
<result property="price" column="price"/>
<result property="goodsName" column="goods_name"/>
<result property="typeId" column="type_id"/>
</resultMap>
<select id="findByTypeId" parameterType="integer" resultMap="Product2Map">
select *
from heima_ssm_book.ch4_product
where type_id = #{typeId}
</select>
</mapper>测试代码:
/**
* 案例测试
*/
@Test
public void findWithProduct2Test() {
SqlSession sqlSession = MybatisUtils.getSession();
CategoryMapper categoryMapper = sqlSession.getMapper(CategoryMapper.class);
List<Category> withProduct2 = categoryMapper.findWithProduct2("白色家电");
withProduct2.forEach(System.out::println);
}结果:

嵌套结果
创建 mapper 接口和 xml:
package io.weew12.github.mapper;
import io.weew12.github.pojo.Category;
import java.util.List;
public interface CategoryMapper {
List<Category> findWithProduct2_2(String typeName);
}<?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="io.weew12.github.mapper.CategoryMapper">
<resultMap id="CategoryWithProduct2_2" type="cateGory">
<id property="id" column="type_id"/>
<result property="typeName" column="type_name"/>
<collection property="product2List" ofType="product2">
<id property="id" column="id"/>
<result property="typeId" column="type_id"/>
<result property="goodsName" column="goods_name"/>
<result property="price" column="price"/>
</collection>
</resultMap>
<select id="findWithProduct2_2" resultMap="CategoryWithProduct2_2">
select p.*, c.type_name
from heima_ssm_book.ch4_product p
left join heima_ssm_book.ch4_category c
on p.type_id = c.id
where c.type_name = #{typeName};
</select>
</mapper>测试代码:
@Test
public void findWithProduct2Test2() {
SqlSession sqlSession = MybatisUtils.getSession();
CategoryMapper categoryMapper = sqlSession.getMapper(CategoryMapper.class);
List<Category> withProduct2 = categoryMapper.findWithProduct2_2("白色家电");
withProduct2.forEach(System.out::println);
}结果:

4.7 本章小结
本章首先对开发中涉及的数据表之间以及对象之间的关联关系做了简要介绍,并由此引出了 MyBatis 框架中对关联关系的处理;然后通过案例对 MyBatis 框架处理实体对象之间的三种关联关系进行了详细讲解;最后讲解了 MyBatis 的缓存机制,包括一级缓存和二级缓存。通过学习本章的内容,读者可以了解数据表之间及对象之间的三种关联关系,熟悉 MyBatis 的缓存机制,并能够在 MyBatis 框架中熟练运用三种关联关系进行查询,熟练配置 MyBatis 缓存,从而提高项目的开发效率。
【思考题】
- 请简述
<collection>子元素的常用属性及其作用。
在 MyBatis 中,<collection> 元素主要用于处理一对多(one-to-many)的关系映射,它允许我们将一个对象集合加载到另一个对象中。<collection> 元素包含几个重要的属性,每个属性都有其特定的作用:
property:这是必需的属性,指定了实体类中的属性名,该属性将被用来存储关联的对象集合。
javaType:指定 property 属性所对应的 Java 类型。当使用泛型时,这个属性通常是可选的;但如果集合类型是未参数化的,则必须显式地设置此属性。
ofType:当 javaType 是一个集合类型(如 List 或 Set)时,ofType 用于指定集合中元素的具体类型。这对于没有使用泛型的情况尤其重要。
select:可以指定一个查询语句的 ID 来执行懒加载或嵌套查询。这意味着实际的数据加载会在需要时通过调用指定的 SQL 语句来完成。
column:如果使用了 select 属性进行嵌套查询,那么可能还需要提供一些额外的列信息给子查询,这些列名可以通过 column 属性来指定。
fetchType:定义了数据获取的方式,支持两种值:“lazy” 和 “eager”。默认情况下,MyBatis 使用懒加载策略,即只有在真正访问相关联的对象时才会去数据库中加载它们。
- 请简述 MyBatis 关联查询映射的两种处理方式。
MyBatis 提供了两种主要的方式来处理关联查询映射问题,分别是:
嵌套结果集(Nested Results):这种方式下,MyBatis 会一次性从数据库中拉取所有相关的数据,并直接将它们组装成所需的对象结构。这通常适用于关联关系比较简单且不需要频繁更改的情况。通过在映射文件中使用 <resultMap> 标签下的 <association> 或 <collection> 标签配合 <id>、<result> 等标签来实现。
嵌套查询(Nested Selects):与嵌套结果集不同,这种方法是通过执行多个 SQL 查询来分别获取主表和从表的数据。首先执行主表的查询,然后对于每一条记录再执行一次或多次额外的查询以获取关联的数据。这种方式的好处在于可以更灵活地控制查询条件以及减少不必要的数据传输量。在 MyBatis 中,通过设置 <association> 或 <collection> 的 select 属性来引用另一个 SQL 映射来实现这一点。
这两种方法各有优缺点,在选择时需根据具体的应用场景和个人偏好来决定最适合的方案。
