MyBatis框架
MyBatis延迟加载策略
在 MyBatis 中,延迟加载(Lazy Loading) 是一种按需加载数据的机制,指在查询主对象时,不立即加载其关联的子对象(或关联数据),而是等到真正需要使用这些关联数据时,才发起数据库查询去加载。这种机制的核心目的是减少不必要的数据库交互,提高系统性能,尤其适用于关联关系复杂或关联数据量大的场景。
延迟加载主要用于关联查询,即通过resultMap中 <association>
(一对一)或 <collection>
(一对多)配置的关联对象。
立即加载和延迟加载的区别,使用一对多的环境举例子。
立即加载:当前查询用户的时候,默认也把该用户所拥有的帐户信息查询出来;
延迟加载:当前查询用户的时候,没有把该用户所拥有的帐户信息查询出来,而是使用帐户数据的时候,再去查询账户的数据。
一对多示例
编写 JavaBean
java
import java.io.Serializable;
public class Account implements Serializable {
private Integer id;
private Integer uid;
private Double money;
// 添加用户属性
private User user;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getUid() {
return uid;
}
public void setUid(Integer uid) {
this.uid = uid;
}
public Double getMoney() {
return money;
}
public void setMoney(Double money) {
this.money = money;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
@Override
public String toString() {
return "Account{" +
"id=" + id +
", uid=" + uid +
", money=" + money +
", user=" + user +
'}';
}
}
java
package com.qcby.domain;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
public class User implements Serializable {
//主键
private Integer id;
private String username;
private Date birthday;
private String sex;
private String address;
// 存储所有的id
private List<Integer> ids;
// 一个用户拥有多个账户(演示一对多查询)
private List<Account> accounts;
// 一个用户拥有多个角色(演示多对多查询)
private List<Role> roles;
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 Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public List<Integer> getIds() {
return ids;
}
public void setIds(List<Integer> ids) {
this.ids = ids;
}
public List<Account> getAccounts() {
return accounts;
}
public void setAccounts(List<Account> accounts) {
this.accounts = accounts;
}
public List<Role> getRoles() {
return roles;
}
public void setRoles(List<Role> roles) {
this.roles = roles;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", birthday=" + birthday +
", sex='" + sex + '\'' +
", address='" + address + '\'' +
", ids=" + ids +
", accounts=" + accounts +
", roles=" + roles +
'}';
}
}
SqlMapConfig_lazy.xml 中开启延迟加载(lazyLoadingEnabled),以及将积极加载(aggressive lazy loading)改为消极加载(按需加载)
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!-- 开启延迟加载 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 将积极加载改为消极加载/按需加载 -->
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
<!-- 配置环境 -->
<environments default="mysql">
<environment id="mysql">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql:///mybatis_db"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<!-- 加载映射的配置文件 -->
<mappers>
<mapper resource="mappers/AccountMapper.xml"/>
<mapper resource="mappers/UserMapper.xml"/>
</mappers>
</configuration>
在AccountMapper.java接口内编写方法
java
import com.qcby.domain.Account;
import java.util.List;
public interface AccountMapper {
public List<Account> findAccountAll();
public List<Account> findAccountAllLazy();
}
编写AccountMapper.xml配置文件
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.qcby.mapper.AccountMapper">
<!--内连接查询-->
<select id="findAccountAll" resultMap="accountMap">
select a.*,u.username,u.sex from account as a, user as u where a.uid=u.id;
</select>
<!--配置resultMap标签 目的是进行数据封装-->
<resultMap id="accountMap" type="com.qcby.domain.Account">
<result property="id" column="id"/>
<result property="uid" column="uid"/>
<result property="money" column="money"/>
<association property="user" javaType="com.qcby.domain.User">
<result property="username" column="username" />
<result property="sex" column="sex" />
</association>
</resultMap>
<!--延迟加载-->
<select id="findAccountAllLazy" resultMap="accountlazyMap">
SELECT * FROM account;
</select>
<resultMap id="accountlazyMap" type="com.qcby.domain.Account">
<result property="id" column="id"/>
<result property="uid" column="uid"/>
<result property="money" column="money"/>
<!--配置多对一的延迟加载(Account关联的user集合,对user属性进行数据封装)-->
<association property="user" javaType="com.qcby.domain.User" column="uid" select="com.qcby.mapper.UserMapper.findById" fetchType="lazy"/>
</resultMap>
</mapper>
在 resultMap 的关联标签中配置延迟加载:
column="uid" 即查询user时需要传递的参数,select="com.qcby.mapper.UserMapper.findById" 指定加载user对象时要调用的SQL语句,fetchType 属性是延迟加载的局部配置方式,lazy表示延迟加载、eager立即加载,fetchType="lazy"只有明确访问关联对象的属性时才会触发关联对象的加载,进一步减少不必要的数据库查询。
其中 UserMapper.findById 的查询语句如下:

测试方法
java
import com.qcby.domain.Account;
import com.qcby.mapper.AccountMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Test;
import java.io.InputStream;
import java.util.List;
public class UserTest_lazy {
@Test
public void testfindRoleALL(){
try {
InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig_lazy.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = factory.openSession();
AccountMapper mapper = session.getMapper(AccountMapper.class);
List<Account> accounts = mapper.findAccountAll();
for (Account account : accounts) {
System.out.println(account);
System.out.println(account.getMoney());
System.out.println(account.getUser().getUsername());
System.out.println("==============");
}
//关闭资源
session.close();
inputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 测试延迟加载的测试方法
*/
@Test
public void testfindAccountlazyALL(){
try {
InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig_lazy.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = factory.openSession();
AccountMapper mapper = session.getMapper(AccountMapper.class);
List<Account> list = mapper.findAccountAllLazy();
for (Account account : list) {
System.out.println(account.getMoney());
//System.out.println(account.getUser().getUsername());
System.out.println("=============================");
System.out.println("");
}
//关闭资源
session.close();
inputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
实现效果:
运行 testfindRoleALL() 方法,立即加载,通过内连接一次性查询账户和关联的用户信息

运行 testfindAccountlazyALL() 方法,延迟加载,先查询账户信息,当需要用户信息时再单独查询,减少不必要的数据库交互
输出 user.getUsername() 时,不会触发关联对象的加载,只执行 SELECT * FROM account;

输出 user.getAccounts().size() 时,第一步固定执行查询所有账户信息 SELECT * FROM account;,访问到Account对象的user属性触发延迟加载,第二步执行子查询语句 select * from user where id = ?; ,其中?会被替换为传入的column="uid"的具体账户的uid值

一对多示例
UserMapper.java 接口添加方法
java
import com.qcby.domain.User;
import java.util.List;
public interface UserMapper {
//一对多延迟加载查询
public List<User> findUserAllLazy();
}
UserMapper.xml 配置文件中添加
xml
<!-- 一对多延迟加载 -->
<select id="findUserAllLazy" resultMap="UserAlllazy">
SELECT * FROM user;
</select>
<resultMap id="UserAlllazy" type="com.qcby.domain.User">
<result property="id" column="id"/>
<result property="username" column="username"/>
<result property="birthday" column="birthday"/>
<result property="sex" column="sex"/>
<result property="address" column="address"/>
<!-- 配置一对多的延迟加载(User关联的accounts集合,对accounts属性进行数据封装)-->
<collection property="accounts" ofType="com.qcby.domain.Account" column="id" select="com.qcby.mapper.AccountMapper.findAccountById" fetchType="lazy"/>
</resultMap>
AccountMapper.xml 配置文件中添加
xml
<!-- 根据用户id(uid)查询该用户的所有账户 -->
<select id="findAccountById" parameterType="int" resultType="com.qcby.domain.Account">
SELECT * FROM account where uid=#{uid};
</select>
SqlMapConfig_lazy.xml 配置文件内容不变
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!-- 开启延迟加载 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 将积极加载改为消极加载/按需加载 -->
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
<!-- 配置环境 -->
<environments default="mysql">
<environment id="mysql">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql:///mybatis_db"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<!-- 加载映射的配置文件 -->
<mappers>
<mapper resource="mappers/AccountMapper.xml"/>
<mapper resource="mappers/UserMapper.xml"/>
</mappers>
</configuration>
测试方法
java
import com.qcby.domain.Account;
import com.qcby.domain.User;
import com.qcby.mapper.AccountMapper;
import com.qcby.mapper.UserMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Test;
import java.io.InputStream;
import java.util.List;
public class UserTest_lazy {
@Test
public void testfindUserlazyALL(){
try {
InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig_lazy.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = factory.openSession();
UserMapper mapper = session.getMapper(UserMapper.class);
List<User> list = mapper.findUserAllLazy();
for (User user : list) {
System.out.println(user.getUsername());
//System.out.println(user.getAccounts().size());
System.out.println("=============================");
System.out.println("");
}
//关闭资源
session.close();
inputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
实现效果:
输出 user.getUsername() 时,不会触发关联对象的加载,只执行 SELECT * FROM user;

输出 user.getAccounts().size() 时,第一步固定执行查询所有用户信息 SELECT * FROM user;,访问到User对象的accounts属性触发延迟加载,第二步执行子查询语句 SELECT * FROM account where uid = ?; ,其中?会被替换为传入的column="id"的具体用户的id值

MyBatis框架的缓存
缓存是指在计算系统中,通过特定的高速存储介质临时存储数据源中频繁访问的数据副本,以实现数据快速复用的机制。其核心原理是利用高速存储介质与数据源之间的访问速度差异,当数据请求发生时,优先从缓存中查询目标数据:若缓存中存在该数据,则直接返回缓存副本,避免对原始数据源的访问;若缓存中不存在该数据,则从数据源获取数据并同步至缓存,为后续可能的重复请求提供基础。
这种机制通过缩短数据访问路径、降低对低速数据源的依赖,有效提升了系统响应速度与整体吞吐量,是计算机领域优化数据访问性能的核心技术之一。
一级缓存
MyBatis 的一级缓存,官方称其为本地缓存(Local Cache),是框架默认启用且无需额外配置的会话级缓存机制,其作用域严格限定在单个 SqlSession 实例的生命周期内。在实现层面,每个 SqlSession 对象内部维护着一个基于 Map 的键值对集合,专门用于存储缓存数据。
其工作流程遵循缓存优先原则:当通过当前 SqlSession 执行查询操作时,MyBatis 会先在一级缓存中进行检索,若缓存中存在对应数据,则直接返回该缓存副本,无需与数据库交互;若缓存中不存在目标数据,则执行数据库查询,获取结果后,会自动将该结果存入当前 SqlSession 的一级缓存中,为后续相同条件的查询提供数据支持。
为保障缓存数据与数据库数据的一致性,一级缓存会被自动维护:当在当前 SqlSession 中执行 INSERT、UPDATE、DELETE 等写操作时,MyBatis 会触发一级缓存的清空机制,避免因数据更新导致缓存中留存旧数据;当 SqlSession 执行关闭、提交或回滚操作时,其对应的一级缓存也会随之失效并释放资源。
这种机制使得一级缓存仅在单个数据库会话内有效,不同 SqlSession 之间的缓存相互隔离、无法共享,从而在减少同一会话内重复查询的数据库访问次数的同时,避免了跨会话的数据一致性风险。
SqlMapConfig_cache.xml 配置文件
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 配置环境 -->
<environments default="mysql">
<environment id="mysql">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql:///mybatis_db"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<!-- 加载映射的配置文件 -->
<mappers>
<mapper resource="mappers/UserMapper.xml"/>
</mappers>
</configuration>
需要注意的是,为比较输出对象的是否为同一对象,我们比较输出对象的引用地址,即 User 类不重写 toString() 方法
测试方法
java
import com.qcby.domain.User;
import com.qcby.mapper.UserMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Test;
import java.io.IOException;
import java.io.InputStream;
public class UserTest_cache {
/**
* 证明一级缓存的存在
*/
@Test
public void run1() throws IOException {
InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig_cache.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = factory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
//通过主键查询
User user = mapper.findById(1);
System.out.println(user);
System.out.println("==================================");
//手动清空缓存
//sqlSession.clearCache();
//再查询一次
User user1=mapper.findById(1);
System.out.println(user1);
sqlSession.close();
inputStream.close();
}
}
尽管进行了两次查询,但日志中仅出现了一条 SQL 语句的执行记录,且两次查询输出的User对象引用地址完全相同,这说明第二次查询并未重新执行 SQL 去数据库获取数据,而是直接复用了第一次查询后缓存到内存中的User对象,符合一级缓存缓存主线程同一会话中相同查询条件的结果的特性。

执行 sqlSession.clearCache();,手动清空缓存,这样日志出现了两条SQL语句,且两次查询输出的User对象引用地址不同

java
import com.qcby.domain.User;
import com.qcby.mapper.UserMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Test;
import java.io.IOException;
import java.io.InputStream;
public class UserTest_cache {
@Test
public void run2() throws IOException {
InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig_cache.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = factory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.findById(1);
System.out.println(user);
System.out.println("==================================");
SqlSession sqlSession1 = factory.openSession();
UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
User user1=mapper1.findById(1);
System.out.println(user1);
sqlSession.close();
inputStream.close();
}
}

两次查询分别在两个独立的SqlSession实例中执行,而 MyBatis 的一级缓存作用域严格限定于单个SqlSession,这两个会话各自维护一个独立且初始为空的缓存。因此,第一次查询命中第一个SqlSession的空缓存,触发数据库访问并生成一条 SQL 日志,结果存入该会话的缓存;第二次查询同样命中第二个SqlSession的空缓存,再次触发数据库访问并生成第二条 SQL 日志,结果存入第二个会话的缓存。由于两次查询返回的是两个不同的User对象实例,因此它们的哈希码标识不同。这一现象清晰地证明了一级缓存的会话隔离性,即缓存数据无法在不同SqlSession之间共享。
二级缓存
MyBatis 的二级缓存是 SqlSessionFactory 级别的缓存,它在查询时优先被检查,如果命中则直接返回数据;若未命中,则继续检查当前 SqlSession 的一级缓存,仍未命中才查询数据库,并将结果先写入一级缓存,待 SqlSession 关闭或提交时,再将一级缓存中的数据同步到二级缓存中,供其他 SqlSession 共享。同时,为保证数据一致性,当同一 Namespace 内执行任何增、删、改操作时,该 Namespace 下的整个二级缓存会被自动清空,从而避免读取到脏数据。
SqlMapConfig_cache.xml 中添加如下配置开启全局缓存开关
xml
<settings>
<!--开启二级缓存-->
<setting name="cacheEnabled" value="true"/>
</settings>
UserMapper.xml 中开启二级缓存,表示该Mapper的Namespace将启用二级缓存
xml
<!--开启二级缓存使用-->
<cache/>
java
package com.qcby;
import com.qcby.domain.User;
import com.qcby.mapper.UserMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Test;
import java.io.IOException;
import java.io.InputStream;
public class UserTest_cache {
@Test
public void run3() throws IOException {
//加载配置文件
InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig_cache.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = factory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.findById(1);
System.out.println(user);
System.out.println("=====================");
//手动清空一级缓存
sqlSession.clearCache();
sqlSession.commit();
//关闭session
sqlSession.close();
SqlSession sqlSession1=factory.openSession();
UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
User user1=mapper1.findById(1);
System.out.println(user1);
sqlSession1.close();
inputStream.close();
}
}

第一个SqlSession执行查询时,因二级缓存和自身一级缓存均为空,故访问数据库并生成SQL日志,查询结果存入一级缓存;在其commit并close后,MyBatis将结果序列化并写入UserMapper对应的二级缓存。第二个SqlSession执行相同查询时,直接命中二级缓存,因此没有SQL日志输出,且控制台打印出"Cache Hit Ratio"表明缓存命中率,证明了跨SqlSession的数据共享;由于从二级缓存中获取数据时,进行了反序列化操作,生成的是一个全新的对象,而不是第一个SqlSession中的那个对象实例,所以两次打印的User对象哈希码不同。
UserMapper.xml 配置文件中设置如下内容
xml
<select id="findById" resultType="com.qcby.domain.User" parameterType="int" useCache="false">
select * from user where id = #{id};
</select>
useCache 属性用于控制当前查询是否使用二级缓存,当Mapper.xml文件通过<cache>
标签开启了二级缓存后,该文件中所有的<select>
语句默认继承此设置,即 useCache="true"。设置 useCache="false" 即禁用当前这条查询语句的二级缓存功能,MyBatis 在执行查询时,会完全跳过二级缓存的检查,直接去查询一级缓存。

需要注意的是如果没有让 User 类实现 Serializable 序列化接口,会抛出 NotSerializableException 异常
