Dapper通过手动JOIN中间表+MultiMapping+字典缓存实现多对多映射,核心是SQL扁平查询、splitOn分割字段、内存重组对象树;需注意LEFT JOIN处理空关联、字段别名防冲突、集合初始化及大数据量性能优化。
Dapper 本身不自动处理多对多关系,但通过手动编写连接查询 + MultiMapping 配合字典缓存,就能干净地映射出带集合的嵌套对象,比如“商品拥有多个分类”或“文章属于多个标签”。
多对多在数据库中一定有中间表(如 ProductCategory、PostTag)。Dapper 要做的不是“自动识别关系”,而是把 JOIN 后的扁平结果,按逻辑重新组织成对象树。
Query 多参数泛型方法,指定主实体、关联实体和返回类型splitOn 值必须设为关联表的第一个字段名(如 CategoryId),告诉 Dapper 从哪一列开始映射第二个类Dictionary 缓存已创建的主对象,避免同一主记录因多条关联行被重复实例化假设三张表:Products(Id, Name)、Categories(Id, Name)、ProductCategories(ProductId, CategoryId):
var sql = @"
SELECT p.Id, p.Name,
c.Id AS CategoryId, c.Name AS CategoryName
FROM Products p
INNER JOIN ProductCategories pc ON p.Id = pc.ProductId
INNER JOIN Categories c ON pc.CategoryId = c.Id";
var products = new Dictionary();
connection.Query(sql,
(product, category) =>
{
if (!products.TryGetValue(product.Id, out var p))
{
p = product;
p.Categories = new List(); // 初始化集合
products.Add(p.Id, p);
}
p.Categories.Add(category);
return p;
},
splitOn: "CategoryId"
);
return products.Values.ToList();
注意:这里 splitOn: "CategoryId" 是关键——因为 SQL 中 c.Id AS CategoryId 是关联表字段的起始列,Dapper 才知道前面是 Product,后面是 Category。
category.Id == null 再决定是否添加AS 别名(如 p.Id AS ProductId, c.Id AS CategoryId),否则 splitOn 无法准确定位Categories)必须在首次创建时初始化,否则 Add() 会报空引用
商品 × 平均 5 个分类 = 5000 行),性能瓶颈在内存组装;数据量大时考虑分页或两次查询(先查主表,再用 IN 查关联)基本上就这些。多对多没那么神秘,本质还是“一次查平、内存聚拢”。只要 SQL 写对、splitOn 指对、字典用对,结构就稳了。