上篇我们把 resultMap 的基础模型、association 一对一、collection 一对多都捋了一遍。如果你已经消化得差不多,那么多表关系映射的下半场就好办多了——这一篇要啃的是真正每天会在业务里遇到的硬骨头:多对多映射、同名列冲突、N+1 查询、动态 SQL 的条件陷阱、二级缓存脏读,还有三个我打包票你迟早会碰到的报错。
我尽量不写教科书式的东西,所有内容都从实际项目里来。代码能直接抄,坑能提前避,这是这篇文章的核心目标。案例统一用商城后台的商品库模型:商品表 product、品牌表 brand、分类表 category、供应商表 supplier,品牌和供应商之间通过一张中间表 brand_supplier 维持多对多关系。后面所有章节都会反复用到这套表,所以先花三十秒把关系看明白。
1. 多对多映射:用两段一对多拼出关系模型
1.1 建表与SQL:一张中间表解决品牌和供应商的关系
先说清楚一个很多人刚接触时的疑问:MyBatis 里到底有没有专门的多对多标签?答案是没有,也不需要。多对多关系在数据库层面就是通过中间表拆成两个一对多,在 MyBatis 里同样如此。你永远不会在 resultMap 里写一个 <many-to-many>,你写的是两个 collection,或者一个 association 套一个 collection。
以品牌和供应商为例,表结构大概是这样的:
sql复制CREATE TABLE brand (
id BIGINT PRIMARY KEY,
brand_name VARCHAR(100),
logo_url VARCHAR(255),
deleted TINYINT DEFAULT 0
);
CREATE TABLE supplier (
id BIGINT PRIMARY KEY,
supplier_name VARCHAR(100),
contact_name VARCHAR(50),
contact_phone VARCHAR(20)
);
CREATE TABLE brand_supplier (
id BIGINT PRIMARY KEY,
brand_id BIGINT,
supplier_id BIGINT
);
查询某个品牌下的所有供应商,SQL 就是三张表 left join:
sql复制SELECT
b.id,
b.brand_name,
b.logo_url,
s.id AS supplier_id,
s.supplier_name,
s.contact_name,
s.contact_phone
FROM brand b
LEFT JOIN brand_supplier bs ON b.id = bs.brand_id
LEFT JOIN supplier s ON bs.supplier_id = s.id
WHERE b.id = #{brandId}
注意供应商表的所有字段我都加了别名,supplier_id、supplier_name。这不是多此一举,是多表映射的第一条生命线。后面第 2 章会专门讲为什么。
对应的 resultMap 长这样:
xml复制<resultMap id="brandMap" type="cn.demo.entity.Brand">
<id property="id" column="id"/>
<result property="brandName" column="brand_name"/>
<result property="logoUrl" column="logo_url"/>
<collection property="supplierList" ofType="cn.demo.entity.Supplier">
<id property="id" column="supplier_id"/>
<result property="supplierName" column="supplier_name"/>
<result property="contactName" column="contact_name"/>
<result property="contactPhone" column="contact_phone"/>
</collection>
</resultMap>
这个写法其实就是一对多,区别只在于数据来源多了一张中间表。很多刚接触的人看到多对多就紧张,其实把它拆开来看,一个 collection 解决"品牌有哪些供应商",反过来再写一个 collection 解决"供应商供应哪些品牌",两边都是标准的一对多。
1.2 resultMap里为什么没写“多对多标签”?中间结果集说了算
为什么不需要多对多标签?因为 MyBatis 的映射机制根本不关心你的表之间是什么关系,它只看两样东西:SQL 返回的列,以及你配置的 resultMap。resultMap 负责把结果集里的每一行,按照 <id> 和 <result> 的配置,组装成一个个 Java 对象;<collection> 只是告诉 MyBatis"这一行里带了一个子对象列表的元素"。
理解这一点特别重要。比如上面查品牌和供应商的 SQL,如果 id 为 1 的品牌有 3 个供应商,那么 SQL 返回 3 行,品牌字段在这 3 行里重复出现。MyBatis 在遍历结果集时,第一次碰到 id=1 时创建 Brand 对象,之后每碰到一个 supplier_id,就把供应商封装好塞进同一个 Brand 对象的 supplierList 里。不去重、不额外查数据库,纯粹靠内存里的对象引用。
所以你在配置 resultMap 时,脑子里应该想的是"结果集长什么样、我按什么列分组合并对象",而不是"表关系是几对几"。这也是为什么 MyBatis 能用一个 resultMap 同时撑起一对一、一对多、多对多的原因——映射规则是通用的,关系只是表设计的
