拓冰建站拓冰建站
首页 / 资讯中心 / 正文

MyBatis 高级映射与延迟加载完全指南

MyBatis 高级映射与延迟加载完全指南一、概述在实际企业级开发中数据库表之间往往存在复杂的关联关系一对一、一对多。MyBatis 作为优秀的持久层框架通过resultMap的高级映射功能提供了优雅的解决方案来处理这些关联关系。同时为了优化性能MyBatis 引入了延迟加载Lazy Loading机制允许我们在真正需要关联数据时才去查询数据库避免不必要的性能开销。本文将深入剖析 MyBatis 的高级映射association、collection及延迟加载的配置与使用并结合N1查询问题给出最佳实践。二、高级映射关联关系处理在 MyBatis 中处理关联关系主要依赖resultMap的两个子标签association处理一对一One-to-One关系。collection处理一对多One-to-Many关系。这两种关联映射都支持两种不同的实现方式嵌套结果Nested Results通过一次JOIN查询将主表和关联表的数据一次性查出来然后进行映射性能最优。嵌套查询Nested Queries先查主表然后通过另一条 SQL 语句去查关联表配合延迟加载实现按需查询。2.1 一对一映射association场景每辆车Car都有一个发动机Engine。实体类publicclassCar{privateLongid;privateStringcarNum;privateStringbrand;privateEngineengine;// 一对一关联// getter/setter...}publicclassEngine{privateLongid;privateStringtype;// V8, 电动privateIntegerpower;// 功率}方式一嵌套结果JOIN 查询推荐原理一次 SQL 搞定性能最高。resultMapidCarEngineMaptypecaridpropertyidcolumnid/resultpropertycarNumcolumncar_num/resultpropertybrandcolumnbrand/!-- 一对一映射关联的 Engine 对象 --associationpropertyenginejavaTypeEngineidpropertyidcolumnengine_id/resultpropertytypecolumnengine_type/resultpropertypowercolumnengine_power//association/resultMapselectidselectCarWithEngineresultMapCarEngineMapSELECT c.id, c.car_num, c.brand, e.id AS engine_id, e.type AS engine_type, e.power AS engine_power FROM t_car c LEFT JOIN t_engine e ON c.id e.car_id WHERE c.id #{id}/select方式二嵌套查询配合延迟加载原理先查 Car再根据car_id单独查 Engine。resultMapidCarEngineMapLazytypecaridpropertyidcolumnid/resultpropertycarNumcolumncar_num/resultpropertybrandcolumnbrand/!-- select指向另一个 Mapper 的查询方法column传给子查询的列名 --associationpropertyenginejavaTypeEngineselectcom.xie.mapper.EngineMapper.selectByCarIdcolumnidfetchTypelazy/!-- lazy延迟加载 --/resultMapselectidselectCarLazyresultMapCarEngineMapLazySELECT id, car_num, brand FROM t_car WHERE id #{id}/select2.2 一对多映射collection场景一辆车Car有多个配件Part。实体类publicclassCar{privateLongid;privateStringcarNum;privateStringbrand;privateListPartparts;// 一对多关联}publicclassPart{privateLongid;privateStringname;privateDoubleprice;}方式一嵌套结果JOIN 查询推荐resultMapidCarPartsMaptypecaridpropertyidcolumnid/resultpropertycarNumcolumncar_num/resultpropertybrandcolumnbrand/!-- 一对多ofType 指定集合中元素的类型 --collectionpropertypartsofTypePartidpropertyidcolumnpart_id/resultpropertynamecolumnpart_name/resultpropertypricecolumnpart_price//collection/resultMapselectidselectCarWithPartsresultMapCarPartsMapSELECT c.id, c.car_num, c.brand, p.id AS part_id, p.name AS part_name, p.price AS part_price FROM t_car c LEFT JOIN t_part p ON c.id p.car_id WHERE c.id #{id}/select注意collection必须使用ofType指定集合泛型类型而不是javaTypejavaType默认就是List。方式二嵌套查询配合延迟加载resultMapidCarPartsMapLazytypecaridpropertyidcolumnid/resultpropertycarNumcolumncar_num/resultpropertybrandcolumnbrand/collectionpropertypartsofTypePartselectcom.xie.mapper.PartMapper.selectByCarIdcolumnidfetchTypelazy//resultMap三、延迟加载Lazy Loading3.1 什么是延迟加载延迟加载是指当 MyBatis 查询主对象时不会立即查询关联对象只有当我们调用关联对象的getter方法如car.getEngine()时MyBatis 才会触发关联查询。3.2 配置方式全局配置mybatis-config.xmlsettings!-- 开启延迟加载默认 false --settingnamelazyLoadingEnabledvaluetrue/!-- 激进加载默认 true如果为 true只要主对象被访问所有关联对象都会被加载。 设为 false 则只会在显式调用 getter 时加载推荐关闭 --settingnameaggressiveLazyLoadingvaluefalse/!-- 触发延迟加载的方法列表用逗号分隔 --settingnamelazyLoadTriggerMethodsvalueequals,clone,hashCode,toString//settings局部配置在association/collection中覆盖通过fetchType属性覆盖全局配置fetchTypeeager立即加载不使用延迟。fetchTypelazy延迟加载优先级高于全局配置。associationpropertyengineselect...selectByCarIdcolumnidfetchTypelazy/!-- 该关联使用延迟加载 --3.3 核心原理面试重点MyBatis 为返回的Car对象生成一个代理对象通过 CGLIB 或 Javassist。当你调用car.getEngine()时代理对象会拦截该调用。它判断engine属性是否为null且未加载如果是则执行预先配置好的selectSQL将查询结果赋值给该属性。返回真实的结果。四、N1 查询问题与性能陷阱4.1 什么是 N1 问题当使用嵌套查询时如果主查询返回了 N 条记录那么为了加载每条记录的关联对象MyBatis 会额外执行 N 次子查询总共执行1 N次 SQL。示例ListCarcarscarMapper.selectAll();// 查询 10 辆车 (1 次 SQL)for(Carcar:cars){// 访问 parts 时会触发 10 次额外的 SQL (N 次 SQL)System.out.println(car.getParts().size());}即便开启了延迟加载N1次查询依然会发生只不过从“启动时立即发生”推迟到了“遍历访问时发生”。4.2 解决方案使用嵌套结果JOIN这是解决 N1 问题的唯一根本方法。通过一次JOIN查询将所有数据主表 关联表一次性查出来MyBatis 通过resultMap自动去重并组装成对象树。selectidselectAllWithPartsresultMapCarPartsMapSELECT c.*, p.* FROM t_car c LEFT JOIN t_part p ON c.id p.car_id/select此时只有1 次 SQL性能最高。4.3 策略选择场景推荐方案理由数据量小 / 必须立即展示嵌套结果JOIN性能最好一次查出所有数据主表数据量大 / 关联字段不常用嵌套查询 延迟加载节省内存和带宽按需加载同时加载多层关联谨慎使用嵌套查询容易导致 N1 爆炸优先使用 JOIN五、高级映射与延迟加载最佳实践5.1 分步查询与延迟加载的实战组合利用延迟加载我们可以将复杂的多表关联查询拆分为多个独立的单表查询提高代码复用性。Mapper 接口publicinterfaceCarMapper{CarselectById(Longid);// 只查 Car}publicinterfaceEngineMapper{EngineselectByCarId(LongcarId);// 只查 Engine}XML 组合resultMapidCarMaptypecaridcolumnidpropertyid/!-- 此处相当于在 CarMapper 中调用了 EngineMapper 的方法 --associationpropertyengineselectcom.xie.mapper.EngineMapper.selectByCarIdcolumnidfetchTypelazy//resultMap这种写法使得 SQL 逻辑清晰且selectByCarId方法可以在其他地方复用。5.2 警惕aggressiveLazyLoading在旧版 MyBatis 中如果aggressiveLazyLoadingtrue只要调用了主对象的任何getter方法即使不是关联属性的 getter都会触发所有延迟加载关联的查询。建议在 MyBatis 3.x 及以上保持aggressiveLazyLoadingfalse虽然默认已改为 false但最好显式配置仅在访问具体关联对象时触发查询。5.3 在 Spring 环境中处理 Session 问题延迟加载需要SqlSession保持开启状态。如果在 Spring MVC 的 Controller 中直接返回实体类而视图层渲染时才触发延迟加载此时 Session 可能已关闭会抛出LazyInitializationException。解决方案使用 Spring 的OpenSessionInViewFilterOSIV模式将 Session 生命周期延长到请求结束。在 Service 层使用Transactional确保事务提交前完成所有关联对象的加载触发加载。六、常见问题排查异常信息原因解决方案LazyInitializationExceptionSession 已关闭后尝试加载懒加载数据配置 OSIV或确保在 Session 开启期间访问关联数据明明配置了lazytrue但还是立即执行了查询aggressiveLazyLoadingtrue或调用了hashCode/equals等方法设置aggressiveLazyLoadingfalse一对多关联中数据重复JOIN 查询时未配置id标签务必在resultMap中配置idMyBatis 依靠它去重查询性能极差使用嵌套查询且命中 N1 场景改用嵌套结果JOIN一次性查询七、总结知识点核心要点一对一association使用javaType处理主表与附属表的关系一对多collection使用ofType处理主表与子表集合的关系嵌套结果Nested Results使用JOIN一次 SQL性能最优推荐优先使用嵌套查询Nested Queries拆分 SQL代码解耦但存在 N1 风险延迟加载通过lazyLoadingEnabled开启配合fetchType精细化控制N1 问题嵌套查询下的性能陷阱必须用 JOIN 重构一句话总结在 MyBatis 中优先使用resultMapJOIN进行关联映射嵌套结果以获取最佳性能当关联数据庞大且访问频率低时借助select属性实现分步查询并开启延迟加载嵌套查询来优化内存与响应时间。掌握高级映射与延迟加载的原理不仅能让你写出高性能的数据库访问代码更能让你在系统性能调优时游刃有余。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门