Dapper中查询结果集映射填充含多个集合属性的实体类
Dapper 多集合属性映射实现方案
Dapper 原生splitOn逐行映射逻辑,本身不直接支持一次映射多个独立的一对多集合。你原有代码失效的核心原因是:两个一对多关系直接 JOIN 会产生笛卡尔积,生成大量冗余重复行,同时没有做主对象缓存、空值过滤和重复项判断,最终会得到包含空值、重复项的错误结果。
方案一:多结果集映射(优先推荐)
这是 Dapper 处理多一对多关联的标准实践,完全避免笛卡尔积问题,性能最好、逻辑最不容易出错。核心思路是一次数据库执行返回主表、两个子表共三个结果集,在内存中按外键关联填充集合。
using (var con = new SqlConnection(connectionString)) { con.Open(); // 单次执行返回三个结果集,避免多表JOIN产生的笛卡尔积 var sql = @" SELECT QCLogId, Stage, PassQCTesting FROM dbo.QCLog; SELECT QCLogId, TesterUserId AS PersonId FROM dbo.QCLogTester; SELECT QCLogId, InstrumentNumb FROM dbo.QCLogInstrument; "; using var multiReader = con.QueryMultiple(sql); // 分别读取三类数据 var qcLogs = multiReader.Read<QCLogData>().ToList(); var testers = multiReader.Read<_QCLogPersonFK>().ToList(); var instruments = multiReader.Read<_QCInstrumentFK>().ToList(); // 内存关联填充集合 foreach (var log in qcLogs) { log.Testers = testers .Where(t => t.QCLogId == log.QCLogId) .Select(t => new QCLogPerson { PersonId = t.PersonId }) .ToList(); log.Instruments = instruments .Where(i => i.QCLogId == log.QCLogId) .Select(i => new QCInstrument { InstrumentNumb = i.InstrumentNumb }) .ToList(); } return qcLogs; } // 内部临时辅助类,用于接收带外键的子表查询结果 internal class _QCLogPersonFK : QCLogPerson { public int QCLogId { get; set; } } internal class _QCInstrumentFK : QCInstrument { public int QCLogId { get; set; } }
这个方案的优势:
- 无冗余数据传输,不会产生笛卡尔积,数据量越大性能优势越明显
- 不需要编写复杂的去重逻辑,不会出现重复子项
- 代码结构清晰,后续新增关联集合只需要追加一段查询和关联逻辑即可,维护成本低
方案二:单查询+缓存映射(仅适合小数据量场景)
如果必须使用单 JOIN 查询的方式,可以通过字典缓存已映射的主对象,逐行判断去重后填充集合。
using (var con = new SqlConnection(connectionString)) { var sql = @"SELECT QCL.QCLogId, Stage, PassQCTesting, TesterUserId AS PersonId, InstrumentNumb FROM dbo.QCLog QCL LEFT JOIN dbo.QCLogTester QCT ON QCL.QCLogId = QCT.QCLogId LEFT JOIN dbo.QCLogInstrument QCINSTR ON QCL.QCLogId = QCINSTR.QCLogId;"; var logCache = new Dictionary<int, QCLogData>(); var result = con.Query<QCLogData, QCLogPerson, QCInstrument, QCLogData>( sql, map: (qcLog, tester, instrument) => { // 缓存命中则直接取已存在的主对象,避免重复创建 if (!logCache.TryGetValue(qcLog.QCLogId, out var existingLog)) { existingLog = qcLog; existingLog.Testers = new List<QCLogPerson>(); existingLog.Instruments = new List<QCInstrument>(); logCache.Add(existingLog.QCLogId, existingLog); } // 过滤空值、重复值后添加测试人员 if (tester?.PersonId > 0 && !existingLog.Testers.Any(t => t.PersonId == tester.PersonId)) { existingLog.Testers.Add(tester); } // 过滤空值、重复值后添加仪器 if (!string.IsNullOrWhiteSpace(instrument?.InstrumentNumb) && !existingLog.Instruments.Any(i => i.InstrumentNumb == instrument.InstrumentNumb)) { existingLog.Instruments.Add(instrument); } return existingLog; }, splitOn: "PersonId, InstrumentNumb" ).Distinct().ToList(); return result; }
这个方案的缺陷:
- 两个一对多关联会产生「子表1行数 * 子表2行数」的冗余行,比如某条主记录有2个测试人员、3台仪器,就会返回6行重复主数据,数据量大时性能下降明显
- 需要手动编写空值过滤、重复值判断逻辑,漏写就会产生脏数据
原有代码问题说明
- 没有主对象缓存:每一行数据映射时都会生成新的
QCLogData实例,最终得到大量重复主记录 - 没有空关联过滤:左连接产生的null值(比如没有绑定测试人员的记录)也会被加入集合,生成无效空对象
- 分组逻辑错误:直接调用
Testers.Single()会因为笛卡尔积产生的重复项抛出异常
内容的提问来源于stack exchange,提问作者RobRid
相关产品推荐
相关产品推荐

