MyBatis框架—延迟加载与多级缓存

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 异常

相关推荐
老华带你飞2 小时前
小区服务|基于Java+vue的小区服务管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·论文·毕设·小区服务管理系统
数据知道2 小时前
Go基础:文件与文件夹操作详解
开发语言·后端·golang·go语言
华仔啊2 小时前
Spring 配置混乱?搞懂这两个核心组件,问题真能少一半
java·后端·spring
喂完待续2 小时前
【序列晋升】45 Spring Data Elasticsearch 实战:3 个核心方案破解索引管理与复杂查询痛点,告别低效开发
java·后端·spring·big data·spring data·序列晋升
郑重其事,鹏程万里2 小时前
commons-exec
java
龙茶清欢2 小时前
具有实际开发参考意义的 MyBatis-Plus BaseEntity 基类示例
java·spring boot·spring cloud·mybatis
神龙斗士2402 小时前
Java 数组的定义与使用
java·开发语言·数据结构·算法
计算机学姐2 小时前
基于微信小程序的扶贫助农系统【2026最新】
java·vue.js·spring boot·mysql·微信小程序·小程序·mybatis
白露与泡影2 小时前
2025互联网大厂高频Java面试真题解析
java·开发语言·面试