作为 Java 后端开发,你是否曾经纠结过:查询用户信息时,要不要把用户关联的订单、地址一起查出来?全部查询性能肯定受影响,可不查又怕背面用到时反复访问数据库。这种"查不查"的两难决议,实在可以通过 MyBatis 的延迟加载机制漂亮解决。那么问题来了,MyBatis 到底支持延迟加载吗?它背后的实现原理又是什么?
MyBatis 的延迟加载支持环境
MyBatis 确实支持延迟加载(Lazy Loading)功能,这是一种按需加载的计谋,可以有用减轻系统负担,提高查询效率。
简朴来说,当我们查询一个实体时,对于它的关联对象,不立刻从数据库中加载,而是在第一次真正使用到关联对象时才去数据库查询。如许做可以避免一次性加载过多数据,尤其是在关联关系较多或数据量较大的环境下。
延迟加载的设置方式
MyBatis 提供了两个全局参数来控制延迟加载:
登录后复制 - <settings>
- <!-- 开启延迟加载功能 -->
- <setting name="lazyLoadingEnabled" value="true"/>
- <!-- 设置激进延迟加载策略 -->
- <setting name="aggressiveLazyLoading" value="false"/>
- </settings>
复制代码
- lazyLoadingEnabled:设置为 true 时开启延迟加载功能
- aggressiveLazyLoading:设置为 false 时,按需加载对象属性(只有当调用该属性的 getter 方法时才加载);设置为 true 时,任何对对象方法的调用都会触发全部标志为延迟加载的属性加载
举个简朴例子,当aggressiveLazyLoading=true时:
登录后复制 - User user = userMapper.getUserById(1);
- user.getUsername(); // 仅想获取用户名,但会触发orderList等所有延迟加载属性的加载
- // 或者
- System.out.println(user); // 调用toString()方法,却触发了所有延迟属性的加载
复制代码
因此,生产环境中通常建议保持aggressiveLazyLoading=false,避免不必要的性能损耗。
除了全局设置外,还可以在关联查询中单独设置:
登录后复制 - <!-- association关联查询时使用延迟加载 -->
- <association property="author" column="author_id" select="selectAuthor" fetchType="lazy"/>
- <!-- collection集合查询时使用延迟加载 -->
- <collection property="posts" ofType="Post" column="id" select="selectPostsForBlog" fetchType="lazy"/>
复制代码
通过fetchType属性可以覆盖全局的延迟加载设置,值为lazy表现使用延迟加载,eager表现立刻加载。
延迟加载的触发条件
延迟加载并非任何操作都会触发,详细的触发条件包括:
- 调用延迟属性的 getter 方法:如user.getOrderList()
- 对延迟集合属性进行操作:如orderList.size()、orderList.isEmpty()、遍历操作等
- 仅获取代理对象引用不会触发加载:必须调用其方法才会触发
登录后复制 - User user = userMapper.getUserById(1);
- // 以下操作不会触发延迟加载
- List<Order> orderList = null;
- orderList = user.getOrderList(); // 仅获取引用,不会触发加载
- // 以下操作会触发延迟加载
- int size = user.getOrderList().size(); // 调用size()方法触发加载
- boolean isEmpty = user.getOrderList().isEmpty(); // 调用isEmpty()方法触发加载
- for (Order order : user.getOrderList()) { // 遍历触发加载
- // 处理订单
- }
复制代码
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
延迟加载的实现原理
MyBatis 的延迟加载紧张是通过动态代理实现的。这里涉及两种代理模式:
字节码层面的代理原理
理解代理选择的焦点,需要相识底层实现原理:
- JDK 动态代理:基于接口实现,通过java.lang.reflect.Proxy类在运行时天生接口的代理类。它要求目标类必须实现至少一个接口。
- CGLIB 动态代理:基于字节码天生技术,通过创建目标类的子类来实现代理。CGLIB 在运行时动态修改字节码,重写目标类的方法以插入延迟加载逻辑。
简朴理解:JDK 代理是"实现接口",CGLIB 代理是"继续类"。这就是为什么实现了接口的类优先使用 JDK 代理,而平凡类只能用 CGLIB 代理。
代理机制的选择
MyBatis 会根据目标类是否实现接口选择使用不同的代理机制:
登录后复制 - // MyBatis ProxyFactory选择逻辑(简化版)
- public class ProxyFactory {
- private ProxyFactory() {
- // Prevent Instantiation
- }
- public static Object createProxy(Object target, ResultLoaderMap lazyLoader, Configuration configuration,
- ObjectFactory objectFactory, List<Class<?>> constructorArgTypes,
- List<Object> constructorArgs) {
- // target: 真实对象(如User实例)
- // lazyLoader: 存储延迟加载任务的映射(属性名→加载器)
- // 判断目标类是否为接口或者代理类
- boolean isJdkProxy = target.getClass().getInterfaces().length > 0
- && !Proxy.isProxyClass(target.getClass());
- if (isJdkProxy) {
- // 使用JDK动态代理(优先选择,性能略优且符合Java标准)
- return JdkProxyFactory.createProxy(target, lazyLoader, configuration, objectFactory,
- constructorArgTypes, constructorArgs);
- } else {
- // 使用CGLIB动态代理(目标是非接口的普通类时)
- return CglibProxyFactory.createProxy(target, lazyLoader, configuration, objectFactory,
- constructorArgTypes, constructorArgs);
- }
- }
- }
复制代码
- 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 代理处理逻辑中有很多相似部分,可以抽取公共方法处理:
登录后复制 - // 公共方法处理逻辑
- private Object handleSpecialMethods(Object target, Method method, Object[] args) throws Throwable {
- final String methodName = method.getName();
- if (methodName.equals("equals")) {
- return target.equals(args[0]);
- } else if (methodName.equals("hashCode")) {
- return target.hashCode();
- } else if (methodName.equals("toString")) {
- return target.toString();
- }
- return null; // 不是特殊方法,返回null
- }
- // 然后在代理处理器中调用
- Object result = handleSpecialMethods(target, method, args);
- if (result != null) {
- return result;
- }
- // 处理其他方法...
复制代码
- 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,避免线程间数据污染。
登录后复制 - // ResultLoaderMap简化概念示意
- public class ResultLoaderMap {
- // 存储属性名到ResultLoader的映射
- private final Map<String, LoadPair> loaderMap = new HashMap<>();
- // 检查是否有指定属性的加载器
- public boolean hasLoader(String property) {
- return loaderMap.containsKey(property);
- }
- // 触发指定属性的加载
- public void load(String property) throws SQLException {
- LoadPair pair = loaderMap.get(property);
- if (pair != null) {
- pair.load(); // 执行SQL查询并填充结果
- loaderMap.remove(property); // 加载后移除该加载器
- }
- }
- }
- // 加载器,包含了执行查询所需的全部信息
- class LoadPair {
- private final String property;
- private final MetaObject metaResultObject;
- private final ResultLoader resultLoader;
- public void load() throws SQLException {
- // 执行SQL查询获取结果
- Object value = resultLoader.loadResult();
- // 将结果设置到目标对象的属性上
- metaResultObject.setValue(property, value);
- }
- }
复制代码
- 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)的例子来看看延迟加载如何工作:
实体类界说
登录后复制 - public class User implements Serializable { // 实现Serializable接口避免序列化问题
- private Integer id;
- private String username;
- private List<Order> orderList;
- // getter和setter方法
- }
- public class Order implements Serializable {
- private Integer id;
- private String orderNo;
- private Double amount;
- private Integer userId;
- // getter和setter方法
- }
复制代码
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
MyBatis 设置
登录后复制 - <settings>
- <setting name="lazyLoadingEnabled" value="true"/>
- <setting name="aggressiveLazyLoading" value="false"/>
- </settings>
复制代码
登录后复制 - <mapper namespace="com.example.mapper.UserMapper">
- <!-- 查询用户,延迟加载订单信息 -->
- <select id="getUserById" resultMap="userResultMap" parameterType="int">
- SELECT id, username FROM user WHERE id = #{id}
- </select>
- <!-- 根据用户ID查询订单列表 -->
- <select id="getOrdersByUserId" resultType="com.example.entity.Order" parameterType="int">
- SELECT id, order_no, amount, user_id FROM orders WHERE user_id = #{userId}
- </select>
- <resultMap id="userResultMap" type="com.example.entity.User">
- <id property="id" column="id"/>
- <result property="username" column="username"/>
- <!-- 配置延迟加载 -->
- <collection property="orderList" ofType="com.example.entity.Order"
- column="id" select="getOrdersByUserId" fetchType="lazy"/>
- </resultMap>
- </mapper>
复制代码
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
实行过程与事务
工具类及代码演示
首先,需要一个 MyBatis 工具类来获取 SqlSession:
登录后复制 - 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 java.io.IOException;
- import java.io.InputStream;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- public class MyBatisUtil {
- private static final Logger log = LoggerFactory.getLogger(MyBatisUtil.class);
- private static final SqlSessionFactory sqlSessionFactory;
- static {
- try (InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml")) {
- sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
- } catch (IOException e) {
- log.error("MyBatis配置加载失败", e);
- throw new RuntimeException("MyBatis配置加载失败", e);
- }
- }
- public static SqlSession getSqlSession() {
- return sqlSessionFactory.openSession();
- }
- }
复制代码
- 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 扫描。
然后,使用这个工具类编写延迟加载示例:
登录后复制 - public class LazyLoadingDemo {
- public static void main(String[] args) {
- // 使用try-with-resources确保SqlSession正确关闭
- try (SqlSession sqlSession = MyBatisUtil.getSqlSession()) {
- UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
- // 查询用户信息
- User user = userMapper.getUserById(1);
- System.out.println("用户名: " + user.getUsername());
- // 此时还没有执行订单查询的SQL
- System.out.println("=== 分割线,以上SQL不包含订单查询 ===");
- // 访问订单信息时,才会触发延迟加载,执行订单查询SQL
- // 注意:延迟加载依赖活动的SqlSession,建议在会话关闭前完成所有延迟属性的访问
- List<Order> orderList = user.getOrderList();
- System.out.println("订单数量: " + orderList.size());
- // 后续再次访问不会触发SQL查询,因为已缓存在一级缓存中
- System.out.println("再次访问订单: " + user.getOrderList().size());
- } // SqlSession自动关闭
- // 注意:在此处访问user.getOrderList()会抛出异常
- // 因为延迟加载依赖活动的SqlSession
- }
- }
复制代码
- 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. 使用显式即时加载
在明确需要关联数据的场景下,可以显式指定即时加载:
登录后复制 - <collection property="orderList" ofType="Order" column="id"
- select="getOrdersByUserId" fetchType="eager"/>
复制代码
需要注意的是,fetchType="eager"并不是在 SQL 层面使用 JOIN 查询,而是在主查询完成后立刻实行关联查询。本质上是"分步加载",但不需要等到属性被访问时才加载。
2. 使用 MyBatis 的批量查询功能
MyBatis 提供了多种批量查询方式来解决 N+1 问题:
a) 使用 multiple column 参数传递多个值进行批量查询
登录后复制 - <!-- 配置批量查询的映射 -->
- <collection property="orders" ofType="Order"
- column="{userId=id, userName=username}" select="getOrdersByUserParams"/>
- <!-- 批量查询方法接收多个参数 -->
- <select id="getOrdersByUserParams" resultType="Order">
- SELECT * FROM orders
- WHERE user_id = #{userId}
- AND create_by = #{userName}
- </select>
复制代码
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
b) 手动批量查询优化
登录后复制 - // 手动批量查询优化示例
- List<User> users = userMapper.getAllUsers();
- List<Integer> userIds = users.stream().map(User::getId).collect(Collectors.toList());
- List<Order> allOrders = orderMapper.getOrdersByUserIds(userIds); // 1次批量查询
- // 建立用户-订单映射关系
- Map<Integer, List<Order>> orderMap = allOrders.stream()
- .collect(Collectors.groupingBy(Order::getUserId));
- // 处理用户和订单
- for (User user : users) {
- List<Order> userOrders = orderMap.getOrDefault(user.getId(), Collections.emptyList());
- System.out.println("用户" + user.getUsername() + "的订单数量: " + userOrders.size());
- }
复制代码
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
注意:固然 MyBatis 提供了batchSize设置,但它紧张用于优化批量插入/更新操作,对延迟加载的 N+1 问题没有直接资助。延迟加载的子查询仍然是单条实行的,需要通过上述手动批量查询方式优化。
3. N+1 问题的监控与预防
可以通过以下方式监控和预防 N+1 问题:
登录后复制 - // 配置SQL监控
- @Aspect
- @Component
- public class LazyLoadingMonitor {
- private static final Logger log = LoggerFactory.getLogger(LazyLoadingMonitor.class);
- // 可通过配置调整阈值
- @Value("${mybatis.lazy.threshold:10}")
- private long threshold;
- @Around("execution(* com.example.entity.*.get*(..))")
- public Object monitorLazyLoading(ProceedingJoinPoint pjp) throws Throwable {
- String methodName = pjp.getSignature().getName();
- Object target = pjp.getTarget();
- // 判断是否可能触发延迟加载的getter方法
- if (methodName.startsWith("get") && !methodName.equals("getClass")) {
- // 记录方法调用前的时间
- long start = System.currentTimeMillis();
- Object result = pjp.proceed();
- long end = System.currentTimeMillis();
- // 如果执行时间过长,可能触发了延迟加载
- long duration = end - start;
- if (duration > threshold) {
- log.warn("可能的延迟加载: 类={}, 方法={}, 执行时间={}ms",
- target.getClass().getSimpleName(),
- methodName,
- duration);
- }
- return result;
- }
- return pjp.proceed();
- }
- }
复制代码
- 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. 在序列化前触发延迟加载
确保在序列化前已经访问过延迟加载属性,将代理对象转换为真实对象:
登录后复制 - // 引入Jackson依赖
- import com.fasterxml.jackson.core.JsonProcessingException;
- import com.fasterxml.jackson.databind.ObjectMapper;
- public class SerializationHelper {
- private static final Logger log = LoggerFactory.getLogger(SerializationHelper.class);
- private static final ObjectMapper objectMapper = new ObjectMapper();
- public static String prepareForSerialization(User user) {
- try {
- // 在序列化前触发所有延迟加载
- if (user.getOrderList() != null) {
- user.getOrderList().size(); // 触发延迟加载
- }
- // 现在user中的orderList已经是真实数据,可以安全序列化
- return objectMapper.writeValueAsString(user);
- } catch (JsonProcessingException e) {
- log.error("序列化失败", e);
- throw new RuntimeException("序列化失败", e);
- }
- }
- }
复制代码
- 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 或其他序列化工具的自界说序列化功能:
登录后复制 - // 使用Jackson注解忽略代理相关属性
- @JsonIgnoreProperties({"handler", "hibernateLazyInitializer"})
- public class User implements Serializable {
- // 实体类定义
- }
复制代码
延迟加载与事务的关系
延迟加载依赖的SqlSession需与事务作用域一致。假如事务提条件交或回滚,会导致后续的延迟加载无法实行:
登录后复制 - // 正确示例:在同一事务中完成延迟加载
- @Service
- public class UserService {
- @Autowired
- private UserMapper userMapper;
- @Transactional
- public int getUserOrderCount(int userId) {
- User user = userMapper.getUserById(userId);
- // 在同一事务中访问延迟加载属性
- return user.getOrderList().size();
- }
- }
复制代码
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
在 Spring 环境中,可以使用OpenSessionInView模式延长会话生命周期,但这可能导致数据库连接长时间占用,高并发系统中要谨慎使用。
延迟加载与缓存联合使用
MyBatis 的延迟加载与缓存机制可以协同工作,进一步提升性能:
一级缓存(会话级)
- 默认开启,作用域为 SqlSession
- 延迟加载的结果会存入一级缓存,同一会话内重复访问不会触发数据库查询
- 当实行 update、delete、insert 或调用 clearCache()时,一级缓存会被清空
二级缓存(全局)
- 需手动设置<cache/>或<cache-ref/>
- 延迟加载查询的结果也会被二级缓存缓存
- 跨会话访问时可以直接从二级缓存获取
登录后复制 - <mapper namespace="com.example.mapper.UserMapper">
- <!-- 启用二级缓存 -->
- <cache eviction="LRU"
- flushInterval="60000" <!-- 刷新间隔,单位毫秒 -->
- size="1024" <!-- 引用数量 -->
- readOnly="true"/> <!-- 只读设置 -->
- <!-- 映射器配置 -->
- </mapper>
复制代码
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
readOnly=true表现缓存对象不可变,MyBatis 会直接返回缓存对象引用,提升性能;readOnly=false则返回对象副本,保证线程安全。
二级缓存存储的是完备对象(包括延迟加载后的数据),因此需确保延迟加载触发后的数据会被精确序列化并缓存。建议在getUserById等主查询上设置缓存,延迟加载的子查询(如getOrdersByUserId)可通过flushCache="true"保证数据一致性。
延迟加载的实用场景
适合使用延迟加载的场景
- 关联数据使用频率低:如用户详情页的历史订单,只有用户点击"查看订单"时才需要加载
- 大数据量列表查询:只加载主数据,关联数据按需加载,避免一次性加载过多数据
- 层级数据结构:如树形结构,只需要加载当前节点数据,子节点按需加载
- 统计报表的明细数据:报表页面通常只展示汇总数据,详情数据按需加载
不适合使用延迟加载的场景
- 频仍访问关联数据:如订单详情页需同时展示用户和商品信息,此时即时加载更高效
- 批量数据处理:需要处理大量关联数据的场景,延迟加载会导致 N+1 问题
- 无状态服务:如 REST API,每个哀求都会创建新的 Session,延迟加载可能导致会话关闭问题
- 高并发系统:延迟加载依赖会话,可能导致数据库连接长时间占用
复杂关联关系处理
多对多和嵌套加载处理
在处理复杂关联关系如多对多(用户-脚色)或嵌套关系(用户-订单-商品)时,设置原理相似,但需要注意关联条件和层级结构:
登录后复制 - <!-- 用户与角色的多对多关系 -->
- <resultMap id="userWithRolesMap" type="com.example.entity.User">
- <id property="id" column="id"/>
- <result property="username" column="username"/>
- <!-- 通过中间表查询关联角色 -->
- <collection property="roles" ofType="com.example.entity.Role"
- column="id" select="getRolesByUserId" fetchType="lazy"/>
- </resultMap>
- <!-- 嵌套延迟加载:订单-商品 -->
- <resultMap id="orderMap" type="com.example.entity.Order">
- <id property="id" column="id"/>
- <result property="orderNo" column="order_no"/>
- <!-- 嵌套层级的延迟加载 -->
- <collection property="products" ofType="com.example.entity.Product"
- column="id" select="getProductsByOrderId" fetchType="lazy"/>
- </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企服之家,中国第一个企服评测及商务社交产业平台。 |