代码改变世界

实用指南:MyBatis框架—延迟加载与多级缓存

2025-09-28 08:54  tlnshuju  阅读(12)  评论(0)    收藏  举报

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

在这里插入图片描述