MyBatis多表查询实战:resultMap与association/collection映射详解
开头先聊点实在的。做后端开发只要涉及业务表超过一张基本都会遇到MyBatis多表查询。不管你是刚学SSM整合还是已经在Spring Boot项目里写Mapper迟早会被“订单查用户”“文章查评论”“用户查角色”这类需求找上门。这个系列前面六篇把单表CRUD、参数传递、动态SQL、缓存这些基础打过了这一篇专门把“多表查询”这块硬骨头啃干净。我先说一个很多人踩过的坑MyBatis单表查询写得很溜一上多表就懵因为多表查询的核心不是SQL本身——SQL大家都会写JOIN——而是搞清楚MyBatis怎么把关系型数据库的“表间关系”映射成Java对象的“嵌套属性”。这个映射关系配置好了业务代码写起来才顺手配置不好轻则查出来一堆null重则N1查询直接把数据库拖垮。这篇内容适合正在学MyBatis的初学者也适合写了一阵子但一直被resultMap折磨的开发者。我会把多表查询的两种实现路线、resultMap的完整配置逻辑、一对多/多对一/多对多的落地写法都拆开讲最后补一份我在实际项目中踩过的问题排查记录。全程不用框架自动生成的代码全部手动配置保证你能看懂每一行在干什么。1. 多表查询的整体思路与两种实现路线1.1 先理清楚“表关系”和“对象关系”的差异做多表查询前先想清楚一个问题数据库层面的表和Java对象层面的类描述关系的方式完全不一样。数据库里两张表产生关联靠的是外键列。比如order表里有个user_id通过这个字段就能JOIN出用户的姓名电话。但Java对象不是这样表达的——Order类里通常直接放一个User对象或者ListUser甚至更常见的Order里含UserUser里含ListOrder。这种嵌套结构在数据库里根本不存在纯粹是面向对象的设计。MyBatis多表查询要做的事情就是把这两种表达方式之间的沟壑填平。填平工作发生在两个地方SQL语句负责把多张表的数据查出来resultMap负责把查询结果集里的每一列塞到Java对象的对应属性里去。这两个环节任何一步没做好结果都不对。我见过很多新手写多表查询SQL写对了查询结果也正常但拿到的Java对象里嵌套属性全是null。为什么因为SELECT后面的列名和resultMap里的property对应不上或者压根没配置resultMap只用了resultType。这里先立个结论多表查询只要涉及嵌套对象用resultType基本就是给自己挖坑正确做法是显式定义resultMap。1.2 MyBatis提供的两条路线嵌套结果映射与嵌套查询MyBatis官方文档里多表查询提供了两种实现方式名字略有差异。我习惯把它们叫“嵌套结果映射”和“嵌套查询”这也是国内社区使用最广的说法。嵌套结果映射的核心思路是用一条多表JOIN的SQL把需要的字段全部查出来然后通过resultMap里的association和collection标签把这“一大平铺的结果集”重新组装成嵌套的Java对象。打个比方这就像把一整块拼图的碎片全部倒在桌面上然后按照图纸把它们拼成一个完整的图案。它只需要执行一条SQL语句性能好是优先选择的方案。嵌套查询则不同。它先执行一条SQL查出主表数据然后根据主表数据里的某个字段通常是外键再去执行另一条查询语句。这个场景有点像查快递先根据订单号查出物流单号再根据物流单号去查具体轨迹。好处是SQL逻辑清晰每个查询职责单一坏处是如果主表有100条数据可能触发100次额外查询这就是臭名昭著的N1问题。两种路线怎么选我的经验是关联表数量三张以内、数据量可控、页面一次展示的场景无脑用嵌套结果映射需要懒加载、或者主表数据多但每次只展示少量关联信息的场景考虑嵌套查询但一定要配合懒加载配置使用。接下来我把两条路线的具体配置方法都拆开讲。2. resultMap核心配置与多种关系的映射细节2.1 从单表resultMap到多表resultMap的升级要点先看一个最简单的单表resultMap长什么样resultMap idUserResultMap typecom.example.entity.User id columnid propertyid/ result columnusername propertyusername/ result columnemail propertyemail/ /resultMap这个配置的含义很直白数据库列名column对应Java属性property。注意id标签是给主键用的而且很重要——MyBatis会拿id的值来判断两条记录是不是同一个对象尤其在多表嵌套映射时id配置不对会导致数据错乱或者重复。多表查询的resultMap本质上是在这个基础之上增加两类标签association用于一对一的嵌套关系映射一个Java对象。比如一个订单对应一个用户Order里有个User user字段就靠它。collection用于一对多的嵌套关系映射一个Java集合。比如一个用户有多条订单User里有个ListOrder orders字段就靠它。这两个标签是MyBatis多表查询的核心后面所有内容都围绕它们展开。2.2 association一对一映射订单与用户的经典场景举个最经典的场景查询订单信息同时把下单用户的信息带出来。数据库里order表有user_iduser表有id、username、email。Java对象设计是这样的public class Order { private Integer id; private String orderNo; private Integer userId; private User user; // 嵌套的用户对象 // getter/setter省略 }这里的重点就是一条订单记录里的user_id怎么变成一个User对象塞进Order的user属性里。用嵌套结果映射的写法如下resultMap idOrderWithUserResultMap typecom.example.entity.Order id columnid propertyid/ result columnorder_no propertyorderNo/ result columnuser_id propertyuserId/ association propertyuser javaTypecom.example.entity.User id columnuser_id propertyid/ result columnusername propertyusername/ result columnemail propertyemail/ /association /resultMap select idselectOrderWithUser resultMapOrderWithUserResultMap SELECT o.id, o.order_no, o.user_id, u.id AS user_id, u.username, u.email FROM t_order o LEFT JOIN t_user u ON o.user_id u.id WHERE o.id #{id} /select注意SQL里的一个关键细节我给u.id起了个别名user_id。为什么因为order表和user表都有id字段如果查询结果里出现两个名为id的列MyBatis映射时会分不清谁是谁。这种列名冲突在多表JOIN里非常常见解决办法就两个起别名或者用columnPrefix属性统一加前缀。我习惯在关联表较多时用columnPrefix下面专门说。先解释一下为什么这里user_id既是Order的userId又映射成了User的id因为数据库里order表的user_id列JOIN之后既可以看作是订单表的普通字段又可以看作是user表的主键。在resultMap里这两个含义可以被同时表达这是很多人第一次看会困惑的地方。2.3 columnPrefix解决多表列名冲突的最优方案前面提到多表JOIN常见的列名冲突我用一个实际案例说明。假设要查询用户信息、用户的最新订单、订单里的商品列表——这就涉及三张表关联而且user表和order表都有id字段order表和order_item表也有id字段。如果都用别名SQL会变得很啰嗦SELECT u.id AS user_id, u.username, o.id AS order_id, o.order_no, oi.id AS item_id, oi.product_name FROM t_user u LEFT JOIN t_order o ON o.user_id u.id LEFT JOIN t_order_item oi ON oi.order_id o.id注意这里我同时用了三套前缀user_、order_、item_。如果把这些前缀规则用在resultMap里会是这样的写法resultMap idUserOrderDetailMap typecom.example.entity.User id columnuser_id propertyid/ result columnusername propertyusername/ collection propertyorders ofTypecom.example.entity.Order columnPrefixorder_ id columnid propertyid/ result columnorder_no propertyorderNo/ collection propertyitems ofTypecom.example.entity.OrderItem columnPrefixitem_ id columnid propertyid/ result columnproduct_name propertyproductName/ /collection /collection /resultMapcolumnPrefix的作用是在解析当前嵌套结构时自动去掉这一层的前缀再去匹配列名。比如最内层的id columnid propertyid/实际匹配的是查询结果里的item_id列。这个机制让结果集里的列名可以保持“带前缀”的状态而resultMap里写的是“不带前缀”的属性名各得其所互不混淆。这个技巧是我在实际项目中摸索出来的。当时两张表都有created_time字段配置association后不管怎么调顺序总有一个时间的值是null后来给结果集列名加了不同前缀问题直接消失。所以在这里强烈建议只要是多表JOIN查询查询结果集里所有列统一加表名前缀不要偷懒。2.4 collection一对多映射用户与订单列表的完整配置association处理的是“有一个”collection处理的是“有多个”。还是用用户和订单举例子一个用户有多条订单。public class User { private Integer id; private String username; private ListOrder orders; // 嵌套的订单列表 }对应resultMap里关键配置片段resultMap idUserWithOrdersMap typecom.example.entity.User id columnuser_id propertyid/ result columnusername propertyusername/ collection propertyorders ofTypecom.example.entity.Order id columnorder_id propertyid/ result columnorder_no propertyorderNo/ /collection /resultMap注意两个容易混淆的属性javaType和ofType。在collection里javaType写的是集合本身的类型通常是java.util.List其实不写也行MyBatis能自动推断ofType才是集合里每个元素的类型。很多新手在这里把ofType写成List导致类型转换异常这里强调一下ofType of泛型类型。查询SQL对应写成SELECT u.id AS user_id, u.username, o.id AS order_id, o.order_no FROM t_user u LEFT JOIN t_order o ON o.user_id u.id WHERE u.id #{id}如果查出来用户有3条订单这个查询会返回3行记录用户的基本信息会在每一行重复出现。MyBatis拿到这3行数据后会根据resultMap里的id标签判断哪些列属于同一个User对象同一个User只创建一次然后不断往它的orders集合里添加Order。这个“归并”过程就是嵌套结果映射最核心的机制——也是id标签必须正确配置的原因。2.5 多对多关系用中间表拆解成两个一对多实际业务里纯多对多的场景其实不算多但一旦遇到比如用户-角色学生-课程很多人就不知道怎么写了。这里说一个套路多对多永远不要试图用resultMap直接嵌套三层正确做法是拆成两个一对多来处理。以用户和角色为例数据库里有t_user、t_role、t_user_role三张表。Java对象里User类有一个ListRole roles属性。查询SQL要关联三张表SELECT u.id AS user_id, u.username, r.id AS role_id, r.role_name FROM t_user u LEFT JOIN t_user_role ur ON u.id ur.user_id LEFT JOIN t_role r ON ur.role_id r.id WHERE u.id #{id}resultMap的写法和一对多没有任何区别因为本质上在查出来的结果集里多个角色行是通过user_id归并到同一个User对象下的。你不需要在resultMap里体现t_user_role这张中间表它只是数据查询里的一个桥梁。这就是“多对多转换成两个一对多”的思路。刚才讲的是嵌套结果映射的完整写法。接下来说说另一条路线——嵌套查询select属性以及它和懒加载的关系。2.6 嵌套查询与懒加载什么时候才值得用嵌套查询的基本写法是在association或collection标签上加select属性然后传入column指定的列名作为参数resultMap idOrderWithUserLazyMap typecom.example.entity.Order id columnid propertyid/ result columnorder_no propertyorderNo/ association propertyuser javaTypecom.example.entity.User selectcom.example.mapper.UserMapper.selectById columnuser_id/ /resultMap意思是查询Order时先不查用户数据而是拿到Order的user_id字段把这个值传给UserMapper的selectById方法去查用户。这种写法的优点是非常灵活每个查询方法职责单一代码复用性好——UserMapper.selectById既能被这里调用也能被其他地方直接调用。缺点就是前面说的N1问题。如果一次查出100条订单每条订单都会额外执行一次用户查询总共101条SQL性能惨不忍睹。解决办法有两个一是开启懒加载让关联查询在真正访问到user属性时才执行二是主动避免这种场景比如一次查询多条主表数据时不要用嵌套查询。懒加载的配置在MyBatis全局配置里mybatis: configuration: lazy-loading-enabled: true aggressive-lazy-loading: false第二个参数aggressive-lazy-loading很重要。把它设为false意味着只有真正调用order.getUser()时才会触发关联查询如果保持默认的true那即使只访问order的orderNo也会把user查出来懒加载就形同虚设了。我在项目里实测过两个参数一个不设懒加载不会生效查询次数照样爆炸。一句话总结两条路线的选择标准数据量小、展示场景固定用嵌套结果映射数据量大但每次只访问少量关联数据用嵌套查询加懒加载。没有第三种方案能同时兼顾简单和性能。3. 实操过程订单-用户-订单项-商品完整案例3.1 需求描述与表结构设计理论知识讲再多不如完整走一遍实操。这里我设计一个综合性的案例它同时覆盖一对一、一对多和动态SQL基本能应对日常开发里80%的多表查询需求。业务场景是典型的电商订单详情页展示订单基本信息、下单用户信息、订单中包含的所有商品明细。数据库表一共四张t_user用户表字段有id、username、emailt_order订单表字段有id、order_no、user_id、total_amount、status、create_timet_order_item订单明细表字段有id、order_id、product_id、product_name、product_price、quantityt_product商品表字段有id、product_name、product_price、stockJava实体类对应关系是这样的Order里有User属性一对一、ListOrderItem属性一对多OrderItem里又有Product属性一对一这里我用product_name做了冗余实际项目里更常见的做法是通过product_id去关联Product对象这里为了演示一对多套一对一的场景全部保留。3.2 Mapper接口与XML映射文件的完整实现先写Mapper接口方法public interface OrderMapper { Order selectOrderDetail(Param(orderId) Integer orderId); }再写XML映射文件这是整个案例的核心。我用嵌套结果映射一条SQL搞定所有数据mapper namespacecom.example.mapper.OrderMapper resultMap idOrderDetailMap typecom.example.entity.Order id columnorder_id propertyid/ result columnorder_no propertyorderNo/ result columntotal_amount propertytotalAmount/ result columnstatus propertystatus/ result columncreate_time propertycreateTime/ association propertyuser javaTypecom.example.entity.User id columnuser_id propertyid/ result columnusername propertyusername/ result columnemail propertyemail/ /association collection propertyitems ofTypecom.example.entity.OrderItem id columnitem_id propertyid/ result columnproduct_id propertyproductId/ result columnproduct_name propertyproductName/ result columnproduct_price propertyproductPrice/ result columnquantity propertyquantity/ /collection /resultMap select idselectOrderDetail resultMapOrderDetailMap SELECT o.id AS order_id, o.order_no, o.total_amount, o.status, o.create_time, u.id AS user_id, u.username, u.email, oi.id AS item_id, oi.product_id, oi.product_name, oi.product_price, oi.quantity FROM t_order o LEFT JOIN t_user u ON o.user_id u.id LEFT JOIN t_order_item oi ON oi.order_id o.id WHERE o.id #{orderId} /select /mapper这个案例里没有出现t_product表的JOIN因为我故意在order_item表里冗余了product_name和product_price。这样设计是有原因的实际项目里订单明细一旦生成商品名称和价格就不能跟着商品表变化所以订单明细表快照商品信息是常见做法。如果你要动态获取商品最新信息那就在这个SQL里再LEFT JOIN一张t_product然后加一层association思路完全一样。3.3 查询结果与日志验证写完后运行控制台打印的SQL和执行结果长这样SELECT o.id AS order_id, o.order_no, o.total_amount, o.status, o.create_time, u.id AS user_id, u.username, u.email, oi.id AS item_id, oi.product_id, oi.product_name, oi.product_price, oi.quantity FROM t_order o LEFT JOIN t_user u ON o.user_id u.id LEFT JOIN t_order_item oi ON oi.order_id o.id WHERE o.id ?返回的Java对象结构如下{ id: 1001, orderNo: NO20250101001, user: { id: 1, username: zhangsan, email: zhangsanexample.com }, items: [ { id: 1, productName: iPhone 15, productPrice: 5999.00, quantity: 1 }, { id: 2, productName: 手机壳, productPrice: 49.00, quantity: 2 } ] }数据折叠归并的过程是这样的数据库返回两行记录两条订单明细两行记录的order_id都是1001因此MyBatis判定它们是同一个Order对象不会创建两个Order两行的user_id也都是1所以User对象只创建一次只有item_id不同因此Order.items集合里添加了两个OrderItem对象。这就是嵌套结果映射的“自动去重嵌套组装”过程。3.4 多条件分页查询动态SQL与多表JOIN结合上面是单条订单详情查询。实际业务里还有一类更常见的多表场景列表条件查询。比如按用户名搜索订单按商品名称搜索订单还要分页。这涉及到多表JOIN 动态SQL 分页三件事一起处理。这里我用一个相对完整的方法来演示public interface OrderMapper { ListOrder selectOrderList(Param(username) String username, Param(productName) String productName, Param(status) Integer status); }XML里对应的写法select idselectOrderList resultMapOrderDetailMap SELECT o.id AS order_id, o.order_no, o.total_amount, o.status, o.create_time, u.id AS user_id, u.username, u.email, oi.id AS item_id, oi.product_id, oi.product_name, oi.product_price, oi.quantity FROM t_order o LEFT JOIN t_user u ON o.user_id u.id LEFT JOIN t_order_item oi ON oi.order_id o.id where if testusername ! null and username ! AND u.username LIKE CONCAT(%, #{username}, %) /if if testproductName ! null and productName ! AND oi.product_name LIKE CONCAT(%, #{productName}, %) /if if teststatus ! null AND o.status #{status} /if /where ORDER BY o.create_time DESC /select这里有几个细节要提醒where标签会自动处理掉第一个AND所以每个if里开头的AND可以放心写不需要担心多余AND导致SQL报错。多表查询时条件列尽量加上表别名前缀比如u.username避免多张表出现同名字段时条件匹配错。sql片段可以抽出来复用比如前面resultMap里的SELECT列表字段如果多表查询方法多了建议维护一份sql片段通过sql标签和include标签避免每个方法都写一大串重复的字段和别名。分页可以配合PageHelper插件直接在接口方法前调用PageHelper.startPage(pageNum, pageSize)后面紧跟的查询会被自动拼接LIMIT。这是目前最流行的方案用法我就不展开了但注意多表JOIN分页时PageHelper统计的count语句可能因为JOIN产生重复统计遇到数量不对时需要手写count查询去覆盖。3.5 批量插入与多表查询的联动场景热门搜索词里出现了“mybatis plus 批量插入”在项目里也经常有这样的联动场景批量创建订单时除了插入order表还要批量插入order_item表然后页面立刻要用多表查询把这个订单详情展示出来。批量插入的SQL标准和写法在此不展开只说几个多表联动场景里的坑第一批量插入后必须拿到主键。如果你用JDBC的useGeneratedKeystrue keyPropertyid批量插入时每一条记录的主键都会被回填到对象的id属性里。这是后面插入order_item的order_id的前提。第二如果订单列表同时包含多条订单和各自的明细最安全的做法是外层循环订单逐条插入订单并拿到订单id再循环插入明细。不要试图一次性批量插入所有订单再一次性插入所有明细——那样你很难把明细和订单对应起来。第三插入完成后直接走多表查询因为事务还没提交其他线程查询这些数据会查不到。这也是很多人遇到的“insert没有报错但select查不到”的常见原因之一——事务隔离级别导致通常不是SQL的问题。4. 常见问题与排查技巧实录4.1 resultType误用导致嵌套属性为null这是多表查询最高频的问题没有之一。错误示例是这样的查订单和用户但select标签用resultTypecom.example.entity.Order。结果控制台SQL执行正常返回的Order对象里user属性永远是null。原因很简单resultType的本质是“把查询结果的每一行自动映射到Order的普通属性”它完全没有能力处理关联对象。除非Order类里恰好有userId、username、email这些平铺字段否则嵌套的user对象根本不可能被赋值。解决办法就一句话多表查询涉及嵌套对象时一定用resultMap不要用resultType。判断标准就是——你的查询结果要填充的Java对象里有没有“对象类型”或“集合类型”的属性有就必须上resultMap。4.2 主键id配置缺失导致数据重复另一个我实战中踩过的坑resultMap里的id标签漏配或配错导致collection里的数据重复。典型现象一个用户有3条订单查询结果却返回3个User对象每个User里只有1条订单。这背后的原因前面说过MyBatis根据id来判断记录归并。如果id没有正确映射主键每条记录在MyBatis看来都是不同的User就不会归并。我排查这个问题的经验是在resultMap里每一层嵌套结构都要保证id标签存在且对应真实的唯一主键。即使你的Java实体类没有用到主键也要为它配置正确的column映射。这是“归并机制”的硬性要求。4.3 分页查询数据不准的排查分页是多表查询里容易出问题的领域。最常见的是count查询结果不对比如列表实际显示10条数据但总条数显示成20条。原因在于JOIN产生了“数据膨胀”。比如查订单列表并JOIN订单明细如果一个订单有两条明细查询结果就会变成两行。PageHelper自动生成的count语句是SELECT COUNT(*) FROM (原SQL) AS tmp这个数量也会按膨胀后的行数统计自然不对。解决方式有两种一是分页查询的SQL做去重比如用DISTINCT或者子查询二是提供手写count查询让PageHelper用你写的count查询。我一般选择后者可控性更高。4.4 动态SQL中if test的字符串判断陷阱热门搜索词里出现“mybatis if test indexof”这个知识点在日常动态SQL调试中真的很有用。很多人写多表查询动态条件时用if testusername.indexOf(张) 0来判断字符串包含关系。这个写法在参数为null时会直接抛异常因为null调用indexOf方法会NPE。Safe的做法是复合判断if testusername ! null and username.indexOf(张) 0注意MyBatis的OGNL表达式有短路逻辑前一个条件为false时后面的username.indexOf不会被调用就不会NPE。这一点在日常写动态SQL时很实用。另一个和字符串相关的坑是传入的字符串条件明明有值但if判断不生效。多半是你没注意单引号和双引号的区别。在OGNL里testusername zhangsan才是字符串比较写成testusername \zhangsan\在XML里需要转义容易出错。最简单的办法是统一用单引号包裹字符串别混用。4.5 Spring事务只读模式报错热门搜索词里有条“write operations are not allowed in read-only mode”这是Spring事务里一个非常典型的报错。报错场景通常是方法上标了Transactional(readOnly true)但方法里执行了insert或update操作。这个报错在多表操作场景特别容易出现。比如有人想“查询并更新”订单状态给整个方法标了readOnly true然后调用OrderMapper.updateStatus就直接报这个错。解决方案也很直接把readOnly改成false或者把读写操作拆分成两个不同事务的方法。更规范的做法是查询方法单独标readOnly true写操作单独方法不要标这个属性让事务走默认配置。4.6 逻辑删除数据查询不到“mybatis plus怎么将逻辑删除的数据也查询出来”这个问题本身针对MyBatis-Plus但核心思路在多表查询的排错里同样有价值。遇到这个场景时要意识到框架在你查询时自动拼接了deleted 0条件这是逻辑删除的全局配置生效了。如果你想特殊情况查询deleted 1的数据正确的做法不是去关全局配置而是写自定义SQL用Select注解或XML查询绕过框架的自动拼接。在多表查询里尤其要注意如果两张表都配置了逻辑删除字段JOIN查询时框架可能只给主表拼接删除条件关联表的删除条件需要自己手动在SQL里加上AND u.deleted 0。很多数据对不上多出来或少了记录的问题根源就在这。4.7 控制台SQL日志打印配置排查多表查询问题第一个动作应该是把SQL打印出来看。很多人问我为什么自己的项目控制台看不到SQL基本都是没配日志。MyBatis的SQL日志是按Mapper接口的包名打印的配置很简单在application.yml里加上logging: level: com.example.mapper: debug注意这里的包名写的是Mapper接口所在的包不是XML文件位置更不是Mapper类名。配完重启控制台就能看到每条SQL的完整语句和参数列表。如果想直接看到带参数的可执行SQL可以用p6spy或者MyBatis的插件搜“mybatis打印可执行sql插件”能找到不少方案。我实际用下来感觉配合日志看参数更直观插件偶尔会影响一点性能生产环境不建议开。4.8 大字段与多表JOIN的性能隐患最后说一个很多人等踩了坑才意识到的问题多表JOIN查询时select了不必要的text类型字段。比如t_product表里有product_desc大字段如果你在列表查询里把它select出来会让结果集变得很大IO开销翻倍分页查询速度急剧下降。这个问题的排查思路是列表页通常只需要展示摘要不需要完整的大字段内容。所以多表查询时SELECT里面只放需要的字段不要图省事直接SELECT *。这既是性能优化也是避免大字段和别名冲突的好习惯。前面所有案例里我都显式列出字段并取别名而不是用SELECT *就是这个原因。我自己在实际排查一个订单列表慢查询问题时最后定位到的根因就是多表JOIN了一个带大字段的商品简介把查询耗时从200ms拖到了1.5s。去掉大字段后立刻恢复正常。这个优化手段成本最低收益永远最直接。5. 多表查询的扩展场景与实用心得5.1 打印SQL时一眼定位问题列多表查询出问题第一件事永远是看SQL和参数。但如果你之前没有养成给列取别名的习惯排查时会非常痛苦——两三张表关联后你根本分不清查询结果里某个列到底来自哪张表。我的习惯是所有JOIN查询SELECT里的列统一加表名前缀作为别名比如o.id AS order_id、u.id AS user_id。这样日志里的SQL信息量非常充足看到一个列名就知道它属于谁排查效率提升一个档次。也是基于同样的原因我从不在多表查询里用SELECT *。5.2 新人和老手的差距往往在resultMap设计上写多表查询新人容易直接套用网上的模板老手会先想清楚Java对象的结构再动手。我个人的体会是resultMap的设计应该先于SQL编写而不是反过来。先确定Java对象是“订单里有用户列表”还是“用户里有订单列表”再决定以哪张表为主表以什么字段作为id归并依据最后才是写SQL。很多人容易忽略这一点一上来就写JOIN写到一半发现不知道关联字段映射到哪个属性来回试错浪费大量时间。把这套思路理顺后面所有步骤都是水到渠成的事情。5.3 多对多拆解的正确姿势再补充一个多对多的实用技巧。如果你查询的结果集里需要同时体现“用户拥有的角色”和“角色包含的权限”这就不是“把多对多拆成两个一对多”这么简单的了。更清晰的做法是分层处理用户到角色做一个collection用户在查结果里自动带上角色列表然后每个角色关联的权限再通过单独的查询去补充。实际上大多数业务场景根本不需要在一条SQL里查完所有层级。过度追求“一条SQL搞定一切”往往是性能问题的源头。我的判断标准是三层以内的关联用一条JOIN超过三层优先考虑拆成两次查询在Java代码里组装。代码可读性和执行性能都能提升。5.4 别忽视事务边界对查询的影响多表查询数据不对不一定是SQL的错也可能是事务边界问题。比如你在一个方法里先插入订单再查询订单详情如果两个操作在同一个事务里查询是能看到未提交数据的如果不在同一个事务里查询时数据可能还没提交自然查不到。这类问题的排查思路要清晰先确认你的查询语句单独在数据库里执行是否正常正常再确认是不是事务或延迟加载导致。很多人一遇到数据查不到就怀疑SQL其实SQL没问题是业务代码的调用时机问题。这个系列写到这里多表查询这块就算基本讲透了。从resultMap的两条实现路线到一对一、一对多、多对多的配置细节再到解决列名冲突和动态SQL结合的实操案例最后是各种实际遇到的坑和排查方法。整体看下来你会发现多表查询本质上考验的不是SQL语法而是对“行数据如何折叠成对象结构”这个映射机制的理解。只要把resultMap的归并逻辑想明白后面不管遇到多复杂的关联查询都只是重复套用这些基础模式而已。
