本文主要是介绍MyBatis延迟加载与多级缓存全解析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
《MyBatis延迟加载与多级缓存全解析》文章介绍MyBatis的延迟加载与多级缓存机制,延迟加载按需加载关联数据提升性能,一级缓存会话级默认开启,二级缓存工厂级支持跨会话共享,增删改操作会清空对应缓...
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 = uidRAAxKNFEK;
}
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 = user编程name; } 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 = rolepythons; } @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"> <pythonproperty 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 SqlSessionFact编程oryBuilder().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延迟加载与多级缓存内容请搜索China编程(www.chinasem.cn)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程China编程(www.chinasem.cn)!
这篇关于MyBatis延迟加载与多级缓存全解析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!