当前位置: 代码网 > it编程>编程语言>Java > MyBatis延迟加载与多级缓存全解析

MyBatis延迟加载与多级缓存全解析

2025年09月29日 Java 我要评论
mybatis延迟加载策略在 mybatis 中,延迟加载(lazy loading) 是一种按需加载数据的机制,指在查询主对象时,不立即加载其关联的子对象(或关联数据),而是等到真正需要使用这些关联

mybatis延迟加载策略

在 mybatis 中,延迟加载(lazy loading) 是一种按需加载数据的机制,指在查询主对象时,不立即加载其关联的子对象(或关联数据),而是等到真正需要使用这些关联数据时,才发起数据库查询去加载。这种机制的核心目的是减少不必要的数据库交互,提高系统性能,尤其适用于关联关系复杂或关联数据量大的场景。

延迟加载主要用于关联查询,即通过resultmap中 <association>(一对一)或 <collection>(一对多)配置的关联对象。

立即加载和延迟加载的区别,使用一对多的环境举例子。
立即加载:当前查询用户的时候,默认也把该用户所拥有的帐户信息查询出来;
延迟加载:当前查询用户的时候,没有把该用户所拥有的帐户信息查询出来,而是使用帐户数据的时候,再去查询账户的数据。

一对多示例

编写 javabean

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 +
                '}';
    }
}
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 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接口内编写方法

import com.qcby.domain.account;
import java.util.list;
public interface accountmapper {
    public list<account> findaccountall();
    public list<account> findaccountalllazy();
}

编写accountmapper.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 的查询语句如下:

测试方法

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 接口添加方法

import com.qcby.domain.user;
import java.util.list;
public interface usermapper {
    //一对多延迟加载查询
    public list<user> finduseralllazy();
}

usermapper.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 配置文件中添加

    <!-- 根据用户id(uid)查询该用户的所有账户 -->
    <select id="findaccountbyid" parametertype="int" resulttype="com.qcby.domain.account">
        select  * from account where uid=#{uid};
    </select>

sqlmapconfig_lazy.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>

测试方法

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 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() 方法

测试方法

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对象引用地址不同

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 中添加如下配置开启全局缓存开关

    <settings>
        <!--开启二级缓存-->
        <setting name="cacheenabled" value="true"/>
    </settings>

usermapper.xml 中开启二级缓存,表示该mapper的namespace将启用二级缓存

    <!--开启二级缓存使用-->
    <cache/>
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 配置文件中设置如下内容

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

到此这篇关于mybatis框架—延迟加载与多级缓存的文章就介绍到这了,更多相关mybatis延迟加载与多级缓存内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com