深入理解 MyBatis 延迟加载机制与实现原理

打印 上一主题 下一主题

主题 1998|帖子 1998|积分 5998

作为 Java 后端开发,你是否曾经纠结过:查询用户信息时,要不要把用户关联的订单、地址一起查出来?全部查询性能肯定受影响,可不查又怕背面用到时反复访问数据库。这种"查不查"的两难决议,实在可以通过 MyBatis 的延迟加载机制漂亮解决。那么问题来了,MyBatis 到底支持延迟加载吗?它背后的实现原理又是什么?
     MyBatis 的延迟加载支持环境

     MyBatis 确实支持延迟加载(Lazy Loading)功能,这是一种按需加载的计谋,可以有用减轻系统负担,提高查询效率。
     简朴来说,当我们查询一个实体时,对于它的关联对象,不立刻从数据库中加载,而是在第一次真正使用到关联对象时才去数据库查询。如许做可以避免一次性加载过多数据,尤其是在关联关系较多或数据量较大的环境下。
     延迟加载的设置方式

     MyBatis 提供了两个全局参数来控制延迟加载:
                                   登录后复制                        
  1. <settings>
  2.     <!-- 开启延迟加载功能 -->
  3.     <setting name="lazyLoadingEnabled" value="true"/>
  4.     <!-- 设置激进延迟加载策略 -->
  5.     <setting name="aggressiveLazyLoading" value="false"/>
  6. </settings>
复制代码
      

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
                       

  • lazyLoadingEnabled:设置为 true 时开启延迟加载功能
  • aggressiveLazyLoading:设置为 false 时,按需加载对象属性(只有当调用该属性的 getter 方法时才加载);设置为 true 时,任何对对象方法的调用都会触发全部标志为延迟加载的属性加载
     举个简朴例子,当aggressiveLazyLoading=true时:
                                   登录后复制                        
  1. User user = userMapper.getUserById(1);
  2. user.getUsername(); // 仅想获取用户名,但会触发orderList等所有延迟加载属性的加载
  3. // 或者
  4. System.out.println(user); // 调用toString()方法,却触发了所有延迟属性的加载
复制代码
      

  • 1.
  • 2.
  • 3.
  • 4.
                       因此,生产环境中通常建议保持aggressiveLazyLoading=false,避免不必要的性能损耗。
     除了全局设置外,还可以在关联查询中单独设置:
                                   登录后复制                        
  1. <!-- association关联查询时使用延迟加载 -->
  2. <association property="author" column="author_id" select="selectAuthor" fetchType="lazy"/>
  3. <!-- collection集合查询时使用延迟加载 -->
  4. <collection property="posts" ofType="Post" column="id" select="selectPostsForBlog" fetchType="lazy"/>
复制代码
      

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
                       通过fetchType属性可以覆盖全局的延迟加载设置,值为lazy表现使用延迟加载,eager表现立刻加载。
     延迟加载的触发条件

     延迟加载并非任何操作都会触发,详细的触发条件包括:
     

  • 调用延迟属性的 getter 方法:如user.getOrderList()
  • 对延迟集合属性进行操作:如orderList.size()、orderList.isEmpty()、遍历操作等
  • 仅获取代理对象引用不会触发加载:必须调用其方法才会触发
                                   登录后复制                        
  1. User user = userMapper.getUserById(1);
  2. // 以下操作不会触发延迟加载
  3. List<Order> orderList = null;
  4. orderList = user.getOrderList(); // 仅获取引用,不会触发加载
  5. // 以下操作会触发延迟加载
  6. int size = user.getOrderList().size(); // 调用size()方法触发加载
  7. boolean isEmpty = user.getOrderList().isEmpty(); // 调用isEmpty()方法触发加载
  8. for (Order order : user.getOrderList()) { // 遍历触发加载
  9.     // 处理订单
  10. }
复制代码
      

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
                       延迟加载的实现原理

     MyBatis 的延迟加载紧张是通过动态代理实现的。这里涉及两种代理模式:
     

  • JDK 动态代理
  • CGLIB 动态代理
     

     字节码层面的代理原理

     理解代理选择的焦点,需要相识底层实现原理:
     

  • JDK 动态代理:基于接口实现,通过java.lang.reflect.Proxy类在运行时天生接口的代理类。它要求目标类必须实现至少一个接口。
  • CGLIB 动态代理:基于字节码天生技术,通过创建目标类的子类来实现代理。CGLIB 在运行时动态修改字节码,重写目标类的方法以插入延迟加载逻辑。
     简朴理解:JDK 代理是"实现接口",CGLIB 代理是"继续类"。这就是为什么实现了接口的类优先使用 JDK 代理,而平凡类只能用 CGLIB 代理。
     代理机制的选择

     MyBatis 会根据目标类是否实现接口选择使用不同的代理机制:
                                   登录后复制                        
  1. // MyBatis ProxyFactory选择逻辑(简化版)
  2. public class ProxyFactory {
  3.     private ProxyFactory() {
  4.         // Prevent Instantiation
  5.     }
  6.     public static Object createProxy(Object target, ResultLoaderMap lazyLoader, Configuration configuration,
  7.                                     ObjectFactory objectFactory, List<Class<?>> constructorArgTypes,
  8.                                     List<Object> constructorArgs) {
  9.         // target: 真实对象(如User实例)
  10.         // lazyLoader: 存储延迟加载任务的映射(属性名→加载器)
  11.         // 判断目标类是否为接口或者代理类
  12.         boolean isJdkProxy = target.getClass().getInterfaces().length > 0
  13.             && !Proxy.isProxyClass(target.getClass());
  14.         if (isJdkProxy) {
  15.             // 使用JDK动态代理(优先选择,性能略优且符合Java标准)
  16.             return JdkProxyFactory.createProxy(target, lazyLoader, configuration, objectFactory,
  17.                                               constructorArgTypes, constructorArgs);
  18.         } else {
  19.             // 使用CGLIB动态代理(目标是非接口的普通类时)
  20.             return CglibProxyFactory.createProxy(target, lazyLoader, configuration, objectFactory,
  21.                                                constructorArgTypes, constructorArgs);
  22.         }
  23.     }
  24. }
复制代码
      

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
                       

  • 假如目标类实现了接口,MyBatis 会优先使用 JDK 动态代理(性能更好且符合 Java 标准)
  • 假如目标类没有实现接口,则使用 CGLIB 动态代理
           注意:MyBatis 3.2.8+完全支持 JDK/CGLIB 代理自动切换,早期版本可能需要手动设置代理工厂。MyBatis 自 3.3.0 起,若检测到 classpath 中无 CGLIB 依赖,会自动引入mybatis-cglib-proxy模块(基于 CGLIB 3.2.5),因此 Maven 项目通常无需额外设置。若使用 Gradle 或手动管理依赖,需确保相关 jar 包存在。
          动态代理实现优化

     JDK 和 CGLIB 代理处理逻辑中有很多相似部分,可以抽取公共方法处理:
                                   登录后复制                        
  1. // 公共方法处理逻辑
  2. private Object handleSpecialMethods(Object target, Method method, Object[] args) throws Throwable {
  3.     final String methodName = method.getName();
  4.     if (methodName.equals("equals")) {
  5.         return target.equals(args[0]);
  6.     } else if (methodName.equals("hashCode")) {
  7.         return target.hashCode();
  8.     } else if (methodName.equals("toString")) {
  9.         return target.toString();
  10.     }
  11.     return null; // 不是特殊方法,返回null
  12. }
  13. // 然后在代理处理器中调用
  14. Object result = handleSpecialMethods(target, method, args);
  15. if (result != null) {
  16.     return result;
  17. }
  18. // 处理其他方法...
复制代码
      

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
                       ResultLoaderMap:延迟加载的焦点容器

     ResultLoaderMap是 MyBatis 用于管理延迟加载任务的容器,它存储了属性名与对应的ResultLoader的映射关系。每个延迟属性对应一个ResultLoader,当属性被访问时,通过ResultLoader实行对应的子查询并添补数据。
     ResultLoaderMap是会话级(SqlSession)容器,线程安全由SqlSession的线程隔离性保证,无需额外同步。在高并发场景下,每个哀求使用独立SqlSession,避免线程间数据污染。
                                   登录后复制                        
  1. // ResultLoaderMap简化概念示意
  2. public class ResultLoaderMap {
  3.     // 存储属性名到ResultLoader的映射
  4.     private final Map<String, LoadPair> loaderMap = new HashMap<>();
  5.     // 检查是否有指定属性的加载器
  6.     public boolean hasLoader(String property) {
  7.         return loaderMap.containsKey(property);
  8.     }
  9.     // 触发指定属性的加载
  10.     public void load(String property) throws SQLException {
  11.         LoadPair pair = loaderMap.get(property);
  12.         if (pair != null) {
  13.             pair.load(); // 执行SQL查询并填充结果
  14.             loaderMap.remove(property); // 加载后移除该加载器
  15.         }
  16.     }
  17. }
  18. // 加载器,包含了执行查询所需的全部信息
  19. class LoadPair {
  20.     private final String property;
  21.     private final MetaObject metaResultObject;
  22.     private final ResultLoader resultLoader;
  23.     public void load() throws SQLException {
  24.         // 执行SQL查询获取结果
  25.         Object value = resultLoader.loadResult();
  26.         // 将结果设置到目标对象的属性上
  27.         metaResultObject.setValue(property, value);
  28.     }
  29. }
复制代码
      

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
                       延迟加载的实际案例

     让我们通过一个用户(User)和订单(Order)的例子来看看延迟加载如何工作:
     实体类界说

                                   登录后复制                        
  1. public class User implements Serializable { // 实现Serializable接口避免序列化问题
  2.     private Integer id;
  3.     private String username;
  4.     private List<Order> orderList;
  5.     // getter和setter方法
  6. }
  7. public class Order implements Serializable {
  8.     private Integer id;
  9.     private String orderNo;
  10.     private Double amount;
  11.     private Integer userId;
  12.     // getter和setter方法
  13. }
复制代码
      

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
                       MyBatis 设置

     

  • 首先在 MyBatis 全局设置中启用延迟加载:
                                   登录后复制                        
  1. <settings>
  2.     <setting name="lazyLoadingEnabled" value="true"/>
  3.     <setting name="aggressiveLazyLoading" value="false"/>
  4. </settings>
复制代码
      

  • 1.
  • 2.
  • 3.
  • 4.
                       

  • 然后在 Mapper 文件中设置:
                                   登录后复制                        
  1. <mapper namespace="com.example.mapper.UserMapper">
  2.     <!-- 查询用户,延迟加载订单信息 -->
  3.     <select id="getUserById" resultMap="userResultMap" parameterType="int">
  4.         SELECT id, username FROM user WHERE id = #{id}
  5.     </select>
  6.     <!-- 根据用户ID查询订单列表 -->
  7.     <select id="getOrdersByUserId" resultType="com.example.entity.Order" parameterType="int">
  8.         SELECT id, order_no, amount, user_id FROM orders WHERE user_id = #{userId}
  9.     </select>
  10.     <resultMap id="userResultMap" type="com.example.entity.User">
  11.         <id property="id" column="id"/>
  12.         <result property="username" column="username"/>
  13.         <!-- 配置延迟加载 -->
  14.         <collection property="orderList" ofType="com.example.entity.Order"
  15.                     column="id" select="getOrdersByUserId" fetchType="lazy"/>
  16.     </resultMap>
  17. </mapper>
复制代码
      

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
                       实行过程与事务

     

     工具类及代码演示

     首先,需要一个 MyBatis 工具类来获取 SqlSession:
                                   登录后复制                        
  1. import org.apache.ibatis.io.Resources;
  2. import org.apache.ibatis.session.SqlSession;
  3. import org.apache.ibatis.session.SqlSessionFactory;
  4. import org.apache.ibatis.session.SqlSessionFactoryBuilder;
  5. import java.io.IOException;
  6. import java.io.InputStream;
  7. import org.slf4j.Logger;
  8. import org.slf4j.LoggerFactory;
  9. public class MyBatisUtil {
  10.     private static final Logger log = LoggerFactory.getLogger(MyBatisUtil.class);
  11.     private static final SqlSessionFactory sqlSessionFactory;
  12.     static {
  13.         try (InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml")) {
  14.             sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
  15.         } catch (IOException e) {
  16.             log.error("MyBatis配置加载失败", e);
  17.             throw new RuntimeException("MyBatis配置加载失败", e);
  18.         }
  19.     }
  20.     public static SqlSession getSqlSession() {
  21.         return sqlSessionFactory.openSession();
  22.     }
  23. }
复制代码
      

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
                             注意:需要在类路径下添加mybatis-config.xml设置文件,设置数据源和 Mapper 扫描。
          然后,使用这个工具类编写延迟加载示例:
                                   登录后复制                        
  1. public class LazyLoadingDemo {
  2.     public static void main(String[] args) {
  3.         // 使用try-with-resources确保SqlSession正确关闭
  4.         try (SqlSession sqlSession = MyBatisUtil.getSqlSession()) {
  5.             UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
  6.             // 查询用户信息
  7.             User user = userMapper.getUserById(1);
  8.             System.out.println("用户名: " + user.getUsername());
  9.             // 此时还没有执行订单查询的SQL
  10.             System.out.println("=== 分割线,以上SQL不包含订单查询 ===");
  11.             // 访问订单信息时,才会触发延迟加载,执行订单查询SQL
  12.             // 注意:延迟加载依赖活动的SqlSession,建议在会话关闭前完成所有延迟属性的访问
  13.             List<Order> orderList = user.getOrderList();
  14.             System.out.println("订单数量: " + orderList.size());
  15.             // 后续再次访问不会触发SQL查询,因为已缓存在一级缓存中
  16.             System.out.println("再次访问订单: " + user.getOrderList().size());
  17.         } // SqlSession自动关闭
  18.         // 注意:在此处访问user.getOrderList()会抛出异常
  19.         // 因为延迟加载依赖活动的SqlSession
  20.     }
  21. }
复制代码
      

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
                       延迟加载的优缺点

     优点

     

  • 性能提升:避免一次性加载过多不必要的数据,减少内存占用
  • 按需加载:只有真正需要使用关联数据时才会查询,减少不必要的 IO 操作
  • 降低系统压力:特别是在复杂关联关系或大数据量场景下,可以明显降低系统负担
     缺点

     

  • N+1 问题:当需要遍历一个集归并访问每个元素的延迟加载属性时,会导致主查询 1 次+每个对象的延迟查询 N 次,统共 N+1 次查询
  • 代理对象序列化问题:延迟加载的代理对象序列化时可能会出现问题,尤其是 CGLIB 代理对象
  • 会话关闭后无法加载:延迟加载依赖运动的数据库会话,SqlSession 关闭后无法再加载
     

     解决 N+1 问题的方法

     延迟加载可能导致的 N+1 问题可以通过以下方式解决:
     1. 使用显式即时加载

     在明确需要关联数据的场景下,可以显式指定即时加载:
                                   登录后复制                        
  1. <collection property="orderList" ofType="Order" column="id"
  2.            select="getOrdersByUserId" fetchType="eager"/>
复制代码
      

  • 1.
  • 2.
                       需要注意的是,fetchType="eager"并不是在 SQL 层面使用 JOIN 查询,而是在主查询完成后立刻实行关联查询。本质上是"分步加载",但不需要等到属性被访问时才加载。
     2. 使用 MyBatis 的批量查询功能

     MyBatis 提供了多种批量查询方式来解决 N+1 问题:
     a) 使用 multiple column 参数传递多个值进行批量查询

                                   登录后复制                        
  1. <!-- 配置批量查询的映射 -->
  2. <collection property="orders" ofType="Order"
  3.            column="{userId=id, userName=username}" select="getOrdersByUserParams"/>
  4. <!-- 批量查询方法接收多个参数 -->
  5. <select id="getOrdersByUserParams" resultType="Order">
  6.     SELECT * FROM orders
  7.     WHERE user_id = #{userId}
  8.     AND create_by = #{userName}
  9. </select>
复制代码
      

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
                       b) 手动批量查询优化

                                   登录后复制                        
  1. // 手动批量查询优化示例
  2. List<User> users = userMapper.getAllUsers();
  3. List<Integer> userIds = users.stream().map(User::getId).collect(Collectors.toList());
  4. List<Order> allOrders = orderMapper.getOrdersByUserIds(userIds); // 1次批量查询
  5. // 建立用户-订单映射关系
  6. Map<Integer, List<Order>> orderMap = allOrders.stream()
  7.     .collect(Collectors.groupingBy(Order::getUserId));
  8. // 处理用户和订单
  9. for (User user : users) {
  10.     List<Order> userOrders = orderMap.getOrDefault(user.getId(), Collections.emptyList());
  11.     System.out.println("用户" + user.getUsername() + "的订单数量: " + userOrders.size());
  12. }
复制代码
      

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
                             注意:固然 MyBatis 提供了batchSize设置,但它紧张用于优化批量插入/更新操作,对延迟加载的 N+1 问题没有直接资助。延迟加载的子查询仍然是单条实行的,需要通过上述手动批量查询方式优化。
          3. N+1 问题的监控与预防

     可以通过以下方式监控和预防 N+1 问题:
                                   登录后复制                        
  1. // 配置SQL监控
  2. @Aspect
  3. @Component
  4. public class LazyLoadingMonitor {
  5.     private static final Logger log = LoggerFactory.getLogger(LazyLoadingMonitor.class);
  6.     // 可通过配置调整阈值
  7.     @Value("${mybatis.lazy.threshold:10}")
  8.     private long threshold;
  9.     @Around("execution(* com.example.entity.*.get*(..))")
  10.     public Object monitorLazyLoading(ProceedingJoinPoint pjp) throws Throwable {
  11.         String methodName = pjp.getSignature().getName();
  12.         Object target = pjp.getTarget();
  13.         // 判断是否可能触发延迟加载的getter方法
  14.         if (methodName.startsWith("get") && !methodName.equals("getClass")) {
  15.             // 记录方法调用前的时间
  16.             long start = System.currentTimeMillis();
  17.             Object result = pjp.proceed();
  18.             long end = System.currentTimeMillis();
  19.             // 如果执行时间过长,可能触发了延迟加载
  20.             long duration = end - start;
  21.             if (duration > threshold) {
  22.                 log.warn("可能的延迟加载: 类={}, 方法={}, 执行时间={}ms",
  23.                          target.getClass().getSimpleName(),
  24.                          methodName,
  25.                          duration);
  26.             }
  27.             return result;
  28.         }
  29.         return pjp.proceed();
  30.     }
  31. }
复制代码
      

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
                       也可以使用成熟的监控工具,如 MyBatis Plus 的性能分析插件来监控 SQL 实行。
     代理对象序列化问题及解决方案

     延迟加载使用的代理对象在序列化时可能会遇到问题,尤其是 CGLIB 代理类。CGLIB 天生的代理类名称类似$$EnhancerByCGLIB$$xxx,反序列化时需要相同的类路径和类界说。在分布式系统中(如微服务架构),这种代理类可能无法在不同节点间精确反序列化,导致ClassNotFoundException异常。
     解决方案包括:
     1. 确保实体类实现 Serializable 接口

     全部实体类都应该实现java.io.Serializable接口,包括关联实体类。
     2. 在序列化前触发延迟加载

     确保在序列化前已经访问过延迟加载属性,将代理对象转换为真实对象:
                                   登录后复制                        
  1. // 引入Jackson依赖
  2. import com.fasterxml.jackson.core.JsonProcessingException;
  3. import com.fasterxml.jackson.databind.ObjectMapper;
  4. public class SerializationHelper {
  5.     private static final Logger log = LoggerFactory.getLogger(SerializationHelper.class);
  6.     private static final ObjectMapper objectMapper = new ObjectMapper();
  7.     public static String prepareForSerialization(User user) {
  8.         try {
  9.             // 在序列化前触发所有延迟加载
  10.             if (user.getOrderList() != null) {
  11.                 user.getOrderList().size(); // 触发延迟加载
  12.             }
  13.             // 现在user中的orderList已经是真实数据,可以安全序列化
  14.             return objectMapper.writeValueAsString(user);
  15.         } catch (JsonProcessingException e) {
  16.             log.error("序列化失败", e);
  17.             throw new RuntimeException("序列化失败", e);
  18.         }
  19.     }
  20. }
复制代码
      

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
                       3. 使用自界说序列化计谋

     使用 Jackson 或其他序列化工具的自界说序列化功能:
                                   登录后复制                        
  1. // 使用Jackson注解忽略代理相关属性
  2. @JsonIgnoreProperties({"handler", "hibernateLazyInitializer"})
  3. public class User implements Serializable {
  4.     // 实体类定义
  5. }
复制代码
      

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
                       延迟加载与事务的关系

     延迟加载依赖的SqlSession需与事务作用域一致。假如事务提条件交或回滚,会导致后续的延迟加载无法实行:
                                   登录后复制                        
  1. // 正确示例:在同一事务中完成延迟加载
  2. @Service
  3. public class UserService {
  4.     @Autowired
  5.     private UserMapper userMapper;
  6.     @Transactional
  7.     public int getUserOrderCount(int userId) {
  8.         User user = userMapper.getUserById(userId);
  9.         // 在同一事务中访问延迟加载属性
  10.         return user.getOrderList().size();
  11.     }
  12. }
复制代码
      

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
                       在 Spring 环境中,可以使用OpenSessionInView模式延长会话生命周期,但这可能导致数据库连接长时间占用,高并发系统中要谨慎使用。
     延迟加载与缓存联合使用

     MyBatis 的延迟加载与缓存机制可以协同工作,进一步提升性能:
     一级缓存(会话级)

     

  • 默认开启,作用域为 SqlSession
  • 延迟加载的结果会存入一级缓存,同一会话内重复访问不会触发数据库查询
  • 当实行 update、delete、insert 或调用 clearCache()时,一级缓存会被清空
     二级缓存(全局)

     

  • 需手动设置<cache/>或<cache-ref/>
  • 延迟加载查询的结果也会被二级缓存缓存
  • 跨会话访问时可以直接从二级缓存获取
                                   登录后复制                        
  1. <mapper namespace="com.example.mapper.UserMapper">
  2.     <!-- 启用二级缓存 -->
  3.     <cache eviction="LRU"
  4.           flushInterval="60000"  <!-- 刷新间隔,单位毫秒 -->
  5.           size="1024"           <!-- 引用数量 -->
  6.           readOnly="true"/>     <!-- 只读设置 -->
  7.     <!-- 映射器配置 -->
  8. </mapper>
复制代码
      

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
                       readOnly=true表现缓存对象不可变,MyBatis 会直接返回缓存对象引用,提升性能;readOnly=false则返回对象副本,保证线程安全。
     二级缓存存储的是完备对象(包括延迟加载后的数据),因此需确保延迟加载触发后的数据会被精确序列化并缓存。建议在getUserById等主查询上设置缓存,延迟加载的子查询(如getOrdersByUserId)可通过flushCache="true"保证数据一致性。
     延迟加载的实用场景

     适合使用延迟加载的场景

     

  • 关联数据使用频率低:如用户详情页的历史订单,只有用户点击"查看订单"时才需要加载
  • 大数据量列表查询:只加载主数据,关联数据按需加载,避免一次性加载过多数据
  • 层级数据结构:如树形结构,只需要加载当前节点数据,子节点按需加载
  • 统计报表的明细数据:报表页面通常只展示汇总数据,详情数据按需加载
     不适合使用延迟加载的场景

     

  • 频仍访问关联数据:如订单详情页需同时展示用户和商品信息,此时即时加载更高效
  • 批量数据处理:需要处理大量关联数据的场景,延迟加载会导致 N+1 问题
  • 无状态服务:如 REST API,每个哀求都会创建新的 Session,延迟加载可能导致会话关闭问题
  • 高并发系统:延迟加载依赖会话,可能导致数据库连接长时间占用
     复杂关联关系处理

     多对多和嵌套加载处理

     在处理复杂关联关系如多对多(用户-脚色)或嵌套关系(用户-订单-商品)时,设置原理相似,但需要注意关联条件和层级结构:
                                   登录后复制                        
  1. <!-- 用户与角色的多对多关系 -->
  2. <resultMap id="userWithRolesMap" type="com.example.entity.User">
  3.     <id property="id" column="id"/>
  4.     <result property="username" column="username"/>
  5.     <!-- 通过中间表查询关联角色 -->
  6.     <collection property="roles" ofType="com.example.entity.Role"
  7.                 column="id" select="getRolesByUserId" fetchType="lazy"/>
  8. </resultMap>
  9. <!-- 嵌套延迟加载:订单-商品 -->
  10. <resultMap id="orderMap" type="com.example.entity.Order">
  11.     <id property="id" column="id"/>
  12.     <result property="orderNo" column="order_no"/>
  13.     <!-- 嵌套层级的延迟加载 -->
  14.     <collection property="products" ofType="com.example.entity.Product"
  15.                 column="id" select="getProductsByOrderId" fetchType="lazy"/>
  16. </resultMap>
复制代码
      

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
                       在处理复杂关系时要点:
     

  • 对于多对多关系:通常需要一个额外查询处理中央表连接
  • 对于嵌套层级:需确保每层都精确设置延迟加载,并且会话保持运动状态直到全部层级都访问完毕
     MyBatis 与 Hibernate 延迟加载对比

     对于熟悉 Hibernate 的开发者,相识两者差异有助于更好地使用 MyBatis 的延迟加载:
           
特性
MyBatis
Hibernate
代理实现与性能
基于动态代理(JDK/CGLIB),代理对象创建速度快,但功能相对简朴
基于字节码加强(Javassist/ByteBuddy),初始化较慢但运行性能好
加载方式
通过单独的 select 查询(需手动设置)
支持 JOIN 方式和单表查询两种延迟加载
会话管理
需手动管理 SqlSession 生命周期
通过 Session/EntityManager 自动处理
设置方式
XML 或注解,需明确设置 fetchType
通过映射关系直接控制(如@OneToMany(fetch=FetchType.LAZY))
N+1 解决
需手动批量查询或设置关联查询
提供批处理机制(batch fetching)自动优化
          实际应用建议

     

  • 选择性启用:不是全部场景都适合使用延迟加载,需要根据业务特点选择
  • 公道设置全局设置
     

  • 开发环境可以设置lazyLoadingEnabled=true方便调试
  • 生产环境根据实际性能测试结果决定
  • 尽量保持aggressiveLazyLoading=false,避免非预期的性能问题
     

  • 联合缓存机制:MyBatis 的一级缓存、二级缓存与延迟加载配合使用,可以进一步提升性能
  • 在 Service 层管理好会话:确保访问延迟加载属性时 SqlSession 仍然处于打开状态,或考虑使用 Spring 的OpenSessionInView模式
  • 性能测试:在生产环境摆设前,对延迟加载的性能影响进行充实测试,包括高并发场景
     总结

     我们来用表格总结一下 MyBatis 的延迟加载特性:
           
特性
形貌
支持环境
MyBatis 完全支持延迟加载功能
实现原理
基于动态代理机制(JDK 代理或 CGLIB 代理)
延迟容器
使用 ResultLoaderMap 存储延迟加载任务
全局设置
lazyLoadingEnabled和aggressiveLazyLoading控制
局部控制
通过fetchType属性覆盖全局设置
触发条件
调用 getter 方法、集合操作方法(size/isEmpty)、遍历等
会话依赖
延迟加载依赖运动的 SqlSession 和事务
N+1 优化
批量查询、multiple columns 传参
序列化处理
实现 Serializable 接口、预先触发延迟加载、自界说序列化计谋
与缓存联合
延迟加载结果会进入一/二级缓存,提升后续访问性能
实用场景
关联数据使用频率低、大数据量列表查询、层级数据结构

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

王國慶

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表