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

如何用泛型结合Dapper的SplitOn实现一对多关系映射?

Dapper 泛型一对多关联映射实现方案

问题背景

数据库包含Departments和People表,People通过DepartmentId与Departments建立关联,单条人员数据对应单个部门。已定义如下模型:

public class PersonModel
{
    public int Id { get; set; }
    public string FullName { get; set; }
    public DepartmentModel Department { get; set; }
}

public class DepartmentModel
{
    public int Id { get; set; }
    public string Name { get; set; }
}

非泛型场景下,可通过Dapper的SplitOn参数实现关联对象映射:

public async Task<IEnumerable<PersonModel>> GetPeople(string connectionString)
{
     using var connection = new SqlConnection(connectionString);

     var query = @"SELECT People.Id, People.FullName, 
                         Departments.Id, Departments.Name 
                  FROM People 
                  LEFT JOIN Departments ON People.DepartmentId = Departments.Id";

    return await connection.QueryAsync<PersonModel, DepartmentModel, PersonModel>(
        query, 
        (person, department) => {
            person.Department = department;
            return person;
        }, 
        splitOn: "Id" 
    );
}

但在泛型改造时,因无法预先知晓主模型中子属性的名称(如PersonModel的Department),无法直接完成赋值操作,导致泛型方法无法正常运行。

可行实现方案

方案1:传递属性名称,基于反射赋值

通过调用方传入主模型中子属性的名称,利用反射完成赋值:

public async Task<IEnumerable<TModel>> GetWithSubModel<TModel, TSubModel>(
    string connectionString, 
    string query, 
    string subPropertyName)
{
    using var connection = new SqlConnection(connectionString);

    return await connection.QueryAsync<TModel, TSubModel, TModel>(
        query, 
        (model, subModel) => {
            var property = typeof(TModel).GetProperty(subPropertyName);
            if (property != null && property.PropertyType == typeof(TSubModel))
            {
                property.SetValue(model, subModel);
            }
            return model;
        }, 
        splitOn: "Id"
    );
}

调用示例:

var people = await GetWithSubModel<PersonModel, DepartmentModel>(
    connectionString, 
    @"SELECT People.Id, People.FullName, 
             Departments.Id, Departments.Name 
      FROM People 
      LEFT JOIN Departments ON People.DepartmentId = Departments.Id",
    "Department");

方案2:传递赋值委托,直接定义映射逻辑

让调用方传入一个委托,显式指定主模型与子模型的赋值关系,这种方式更直观且无需反射:

public async Task<IEnumerable<TModel>> GetWithSubModel<TModel, TSubModel>(
    string connectionString, 
    string query, 
    Func<TModel, TSubModel, TModel> mapFunction)
{
    using var connection = new SqlConnection(connectionString);

    return await connection.QueryAsync<TModel, TSubModel, TModel>(
        query, 
        mapFunction, 
        splitOn: "Id"
    );
}

调用示例:

var people = await GetWithSubModel<PersonModel, DepartmentModel>(
    connectionString, 
    @"SELECT People.Id, People.FullName, 
             Departments.Id, Departments.Name 
      FROM People 
      LEFT JOIN Departments ON People.DepartmentId = Departments.Id",
    (person, department) => {
        person.Department = department;
        return person;
    });

方案3:表达式树缓存反射逻辑(高性能场景)

如果该泛型方法会被频繁调用,可通过表达式树提前编译赋值逻辑,避免每次反射带来的性能损耗:

// 缓存已编译的赋值委托
public static class MappingSetterCache
{
    private static readonly Dictionary<(Type MainType, Type SubType, string PropName), Action<object, object>> _setterCache = new();

    public static Action<object, object> GetSetter(Type mainType, Type subType, string propertyName)
    {
        var cacheKey = (mainType, subType, propertyName);
        if (_setterCache.TryGetValue(cacheKey, out var setter))
        {
            return setter;
        }

        var property = mainType.GetProperty(propertyName);
        if (property == null || property.PropertyType != subType)
        {
            throw new ArgumentException($"Property {propertyName} not found on {mainType.Name} or type mismatch.");
        }

        // 构建表达式树
        var mainParam = Expression.Parameter(typeof(object), "mainModel");
        var subParam = Expression.Parameter(typeof(object), "subModel");
        var castMain = Expression.Convert(mainParam, mainType);
        var castSub = Expression.Convert(subParam, subType);
        var assignExpr = Expression.Assign(Expression.Property(castMain, property), castSub);
        
        var lambda = Expression.Lambda<Action<object, object>>(assignExpr, mainParam, subParam);
        setter = lambda.Compile();
        _setterCache[cacheKey] = setter;
        return setter;
    }
}

// 泛型查询方法
public async Task<IEnumerable<TModel>> GetWithSubModel<TModel, TSubModel>(
    string connectionString, 
    string query, 
    string subPropertyName)
{
    using var connection = new SqlConnection(connectionString);
    var setter = MappingSetterCache.GetSetter(typeof(TModel), typeof(TSubModel), subPropertyName);

    return await connection.QueryAsync<TModel, TSubModel, TModel>(
        query, 
        (model, subModel) => {
            setter(model, subModel);
            return model;
        }, 
        splitOn: "Id"
    );
}

内容的提问来源于stack exchange,提问作者George

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 12:58:20