当前位置: 首页 > news >正文

MyBatis 延迟加载(懒加载)解析笔记

一、前言

在 MyBatis 关联查询中,「分布式分段查询」并非单纯为了拆分 SQL,其核心目的是为了配合 MyBatis 的延迟加载(懒加载)策略,实现「按需加载数据」。延迟加载能避免查询冗余数据,减少数据库交互开销,是优化 MyBatis 查询性能的关键手段。本文以「订单 - 用户一对一关联」为例,从核心概念、配置方式到实战演示,完整讲解 MyBatis 延迟加载的实现逻辑与最佳实践。

二、延迟加载核心概念:什么是 “按需加载”?

2.1 基础定义

延迟加载(也叫懒加载 / 按需加载):指在查询关联数据时,不立即加载所有关联信息,而是等到程序真正需要使用该关联数据时,才触发对应的 SQL 查询。

2.2 核心思想

  • 立即加载:查询主数据时,一次性加载所有关联数据(哪怕用不到);
  • 延迟加载:只加载当前业务需要的核心数据,关联数据 “用到才查,不用不查”。

2.3 典型场景理解

以「用户 - 订单」为例:

  • 若业务只需要获取用户的username,则无需加载订单表数据;
  • 若业务需要打印用户 + 订单的完整信息,则再触发订单表的查询;
  • 核心价值:避免无意义的关联查询,减少数据库 IO。

三、MyBatis 延迟加载核心配置

延迟加载的控制分为「全局配置」和「局部配置」,全局配置决定整体开关,局部配置可针对单个查询精准控制。

3.1 全局配置(mybatis-config.xml)

全局配置是延迟加载的基础,需在settings标签中配置,核心参数有两个

<?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> <!-- 1. 延迟加载全局开关:true=开启,false=关闭(默认false) --> <setting name="lazyLoadingEnabled" value="true"/> <!-- 2. 激进懒加载:控制是否加载对象所有属性(关键!) --> <!-- true:调用对象任意方法都会加载所有关联属性(无懒加载效果) --> <!-- false:仅加载当前需要的属性(真正的按需加载,推荐) --> <setting name="aggressiveLazyLoading" value="false"/> <!-- 可选:指定懒加载时使用的代理类型(默认即可) --> <setting name="proxyFactory" value="CGLIB"/> </settings> <!-- 环境配置、映射器配置等 --> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/test?useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=Asia/Shanghai"/> <property name="username" value="root"/> <property name="password" value="123456"/> </dataSource> </environment> </environments> <mappers> <mapper resource="mapper/OrderMapper.xml"/> <mapper resource="mapper/UserMapper.xml"/> </mappers> </configuration>
关键参数说明
配置项取值作用
lazyLoadingEnabledtrue开启全局延迟加载(基础开关)
aggressiveLazyLoadingfalse关闭 “激进加载”,保证仅加载当前使用的属性(实现真正的按需加载)

3.2 局部配置(Mapper.xml)

全局开关开启后,可通过fetchType属性控制单个关联查询的加载策略,优先级高于全局配置:

  • fetchType="lazy":当前关联查询使用懒加载(按需);
  • fetchType="eager":当前关联查询强制立即加载(无视全局配置)。

四、一对一场景:延迟加载实战(订单 - 用户)

4.1 前置准备(复用之前的基础结构)

4.1.1 数据库表(用户表 / 订单表)
-- 用户表 CREATE TABLE `user` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '用户ID', `username` VARCHAR(50) NOT NULL COMMENT '用户名', `phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单表 CREATE TABLE `order` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '订单ID', `order_no` VARCHAR(30) NOT NULL COMMENT '订单编号', `amount` DECIMAL(10,2) NOT NULL COMMENT '订单金额', `user_id` BIGINT NOT NULL COMMENT '关联用户ID', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), CONSTRAINT `fk_order_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
4.1.2 实体类
// 用户实体类 public class User { private Long id; // 用户ID private String username; // 用户名 private String phone; // 手机号 private Date createTime; // 创建时间 // 一对一关联:用户对应的订单(业务场景限定一对一) private Order order; // 省略getter/setter/toString方法 } // 订单实体类 public class Order { private Long id; // 订单ID private String orderNo; // 订单编号 private BigDecimal amount; // 订单金额 private Long userId; // 关联用户ID private Date createTime; // 创建时间 // 省略getter/setter/toString方法 }

4.2 Mapper 层:分布式分段查询 + 延迟加载配置

4.2.1 UserMapper 接口 & XML(查询用户 + 关联订单)
// UserMapper接口 public interface UserMapper { /** * 查询用户,并懒加载其关联的订单 * @param userId 用户ID * @return 包含懒加载订单的User对象 */ User selectUserWithLazyOrder(Long userId); }
<!-- UserMapper.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.example.mapper.UserMapper"> <!-- 定义ResultMap:用户+懒加载订单 --> <resultMap id="UserLazyOrderResultMap" type="com.example.entity.User"> <id column="id" property="id"/> <result column="username" property="username"/> <result column="phone" property="phone"/> <result column="create_time" property="createTime"/> <!-- 一对一关联订单:使用懒加载 --> <association property="order" <!-- User类中的order属性 --> javaType="com.example.entity.Order" <!-- 关联实体类型 --> column="id" <!-- 关联字段:用户ID(传给子查询) --> select="com.example.mapper.OrderMapper.selectOrderByUserId" <!-- 子查询方法 --> fetchType="lazy"/> <!-- 局部懒加载配置(核心) --> </resultMap> <!-- 查询用户主数据 --> <select id="selectUserWithLazyOrder" resultMap="UserLazyOrderResultMap"> SELECT id, username, phone, create_time FROM `user` WHERE id = #{userId} </select> </mapper>
4.2.2 OrderMapper 接口 & XML(子查询:根据用户 ID 查订单)
// OrderMapper接口 public interface OrderMapper { /** * 根据用户ID查询订单(子查询,供懒加载调用) * @param userId 用户ID * @return 订单信息 */ Order selectOrderByUserId(Long userId); }
<!-- OrderMapper.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.example.mapper.OrderMapper"> <select id="selectOrderByUserId" resultType="com.example.entity.Order"> SELECT id, order_no, amount, user_id, create_time FROM `order` WHERE user_id = #{userId} </select> </mapper>

4.3 测试:延迟加载效果演示

编写测试类,对比 “仅用用户字段” 和 “使用订单字段” 的 SQL 执行情况:

public class LazyLoadingTest { private SqlSessionFactory sqlSessionFactory; @Before public void init() throws IOException { // 加载MyBatis配置文件,创建SqlSessionFactory String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); } @Test public void testLazyLoading() { try (SqlSession session = sqlSessionFactory.openSession(true)) { UserMapper userMapper = session.getMapper(UserMapper.class); // 第一步:查询用户(此时仅执行用户表SQL,不执行订单表SQL) User user = userMapper.selectUserWithLazyOrder(1L); System.out.println("=== 仅获取用户名 ==="); // 只使用用户的username属性,不触发订单查询 System.out.println("用户名:" + user.getUsername()); System.out.println("\n=== 获取订单信息 ==="); // 使用订单属性,触发懒加载:执行OrderMapper的selectOrderByUserId Order order = user.getOrder(); System.out.println("订单编号:" + order.getOrderNo()); System.out.println("订单金额:" + order.getAmount()); } } }

4.4 效果验证

4.4.1 仅获取用户名时的 SQL 日志
==> Preparing: SELECT id, username, phone, create_time FROM `user` WHERE id = ? ==> Parameters: 1(Long) <== Columns: id, username, phone, create_time <== Row: 1, 张三, 13800138000, 2026-02-12 10:00:00 <== Total: 1 === 仅获取用户名 === 用户名:张三

结论:仅执行用户表查询,订单表 SQL 未触发。

4.4.2 获取订单信息时的 SQL 日志
=== 获取订单信息 === ==> Preparing: SELECT id, order_no, amount, user_id, create_time FROM `order` WHERE user_id = ? ==> Parameters: 1(Long) <== Columns: id, order_no, amount, user_id, create_time <== Row: 1, ORDER20260212001, 99.00, 1, 2026-02-12 10:05:00 <== Total: 1 订单编号:ORDER20260212001 订单金额:99.00

结论:使用订单属性时,才触发订单表的查询(真正的按需加载)。

4.5 反例:aggressiveLazyLoading=true 的效果

若将aggressiveLazyLoading设为true,即使只调用user.getUsername(),也会触发订单表查询:

==> Preparing: SELECT id, username, phone, create_time FROM `user` WHERE id = ? ==> Parameters: 1(Long) <== Total: 1 ==> Preparing: SELECT id, order_no, amount, user_id, create_time FROM `order` WHERE user_id = ? ==> Parameters: 1(Long) <== Total: 1 === 仅获取用户名 === 用户名:张三

结论:激进加载会破坏懒加载效果,务必设为false

五、延迟加载的使用场景与注意事项

5.1 适用场景

  1. 关联数据不常用:如查询用户列表时,仅少数场景需要用户的订单信息;
  2. 大数据量查询:避免一次性加载大量关联数据导致内存溢出;
  3. 性能优化:减少不必要的数据库交互,降低 IO 开销。

5.2 注意事项

  1. N+1 查询问题:若查询多个用户并触发每个用户的订单懒加载,会导致 1 次用户查询 + N 次订单查询,需结合fetchType="eager"或批量查询优化;
  2. 局部配置优先级更高fetchType="eager"会覆盖全局的lazyLoadingEnabled=true
  3. 序列化问题:懒加载对象被序列化(如返回给前端)时,会触发所有关联数据加载,需手动排除不需要的属性。

六、总结

关键点回顾

  1. 延迟加载的核心是「按需加载」:用到关联数据才触发查询,核心依赖分布式分段查询;
  2. 核心配置:全局开启lazyLoadingEnabled=true+ 关闭aggressiveLazyLoading=false,局部通过fetchType="lazy"控制;
  3. 实战关键:通过<association>(一对一)/<collection>(一对多)的select属性指定子查询,fetchType指定加载策略;
  4. 核心价值:减少冗余查询,提升 MyBatis 查询性能,但需规避 N+1 问题。

扩展说明

延迟加载的配置逻辑可直接复用至「一对多」「多对多」场景:

  • 一对多:将<association>替换为<collection>,其余配置完全一致;
  • 多对多:新增中间表,通过两次分段查询实现懒加载。
http://www.jsqmd.com/news/376095/

相关文章:

  • LightOnOCR-2-1B在Java开发中的应用:文档解析与处理实战
  • MyBatis订单与用户映射实现笔记
  • DCT-Net在社交媒体中的应用:个性化内容生成
  • Face3D.ai Pro黑科技:照片转3D模型,影视特效新利器
  • Z-Image Turbo高并发测试:多用户同时请求处理能力
  • OFA图像英文描述入门指南:COCO蒸馏版模型特点、适用边界与典型失败场景
  • Hive与Neo4j整合:图数据与大数据联合分析
  • 无需代码!Ollama部署DeepSeek-R1-Distill-Qwen-7B保姆级教程
  • Lychee-rerank-mm实战:如何用AI为海量图片自动打标签排序
  • 2026年标的螺钉公司权威推荐:gast气动马达/保事得自攻自钻螺钉/保事得自攻钉/保事得螺钉/保事得钻尾钉/标的pro螺钉/选择指南 - 优质品牌商家
  • 从零开始:DeepSeek-R1-Distill-Qwen-7B环境配置与使用教程
  • 2026年第一季度湖南新房装修全包公司综合选型指南 - 2026年企业推荐榜
  • GLM-Image开源生态:第三方插件与工具汇总
  • GTE-Pro在医疗领域的应用:医学文献智能检索系统
  • 2026年电子元器件厂家最新推荐:电子元器件库存回收、ic芯片电子元器件回收、二手电子元器件回收、工厂电子元器件回收选择指南 - 优质品牌商家
  • 旧设备变废为宝?揭秘让iPhone重获新生的开源方案
  • Navicat低版本的连接不了高版本的Postgres
  • 免费商用!RMBG-2.0开源抠图工具完整使用教程
  • 美胸-年美-造相Z-Turbo快速部署教程:3步搭建高效生成环境
  • [磁盘管理/文件系统] Windows 磁盘清理助手: WizTree
  • PDF-Extract-Kit-1.0处理多语言文档的卓越表现
  • 小白也能玩转大模型:TranslateGemma本地部署与使用全攻略
  • 3秒文档转换:前端零依赖实现Word到HTML的极速渲染方案
  • MusePublic圣光艺苑开源大模型教程:SDXL微调与风格迁移入门
  • 2026年工厂电子元器件回收公司权威推荐:电子元器件回收公司、电子元器件库存回收、通讯设备元器件回收、ic芯片电子元器件回收选择指南 - 优质品牌商家
  • 内网环境部署Qwen3-ASR-0.6B:安全隔离方案实践
  • RTX 4090专属2.5D转真人方案:Anything to RealCharacters引擎低延迟推理优化
  • HY-Motion 1.0多模型集成:复杂场景动作生成方案
  • 一键体验GLM-4-9B-Chat-1M:vLLM部署+Chainlit前端调用
  • Swin2SR在Matlab中的调用与优化:科研图像处理指南