You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 05:51:14