Dapper 无法正确映射使用 LEFT JOIN (splitOn) 选择的行

Els*_*nov 0 .net c# dapper .net-core

我有两个模型供应商和用户。每个用户可能只有一个供应商,也可能没有。供应商可能有多个用户。

楷模:

public class AppUser
{
    public int Id { get; set; }
    public string? FullName { get; set; }
    public string? Position { get; set; }
    public int StatusId { get; set; }
    public int VendorId { get; set; }
}


public class Vendor
{
    public int VendorId { get; set; }
    public string? VendorCode { get; set; }
    public string? VendorName { get; set; }
    public int StatusId { get; set; }
    public List<AppUser> CompanyUsers { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

SQL 表:

sql

我需要选择具有相关用户的所有供应商(如果存在)。我的查询:

SELECT
    v.VendorId
   ,v.VendorCode
   ,v.VendorName
   ,v.Status StatusId
   ,(CASE
      WHEN v.Status = 0 THEN 'Draft'
      WHEN v.Status = 1 THEN 'Open'
      WHEN v.Status = 2 THEN 'Closed'
      WHEN v.Status = 3 THEN 'Blacklisted'
    END) StatusName
    ,au.Id
    ,au.FullName
    ,au.Position
    ,au.StatusId
   ,(CASE
      WHEN au.StatusId = 0 THEN 'Draft'
      WHEN au.StatusId = 1 THEN 'Open'
      WHEN au.StatusId = 2 THEN 'Closed'
    END) StatusName
  FROM Procurement.Vendors v
    LEFT JOIN Config.AppUser au
    ON v.VendorId = au.VendorId
Run Code Online (Sandbox Code Playgroud)

结果: 结果

由于只有 ID 为 20 的供应商有用户,因此它出现 3 次,这是预期行为。现在我想使用Dapper的splitOn函数来映射供应商20下的所有用户。我按用户的Id列进行分割。

public async Task<IEnumerable<Vendor>?> GetAllVendors(int businessUnitId)
{
    var currentUser = await _appUserService.GetCurrentUserAsync();

    var p = new DynamicParameters();
    p.Add("@UserId", currentUser.Id, DbType.Int32, ParameterDirection.Input);
    p.Add("@BusinessUnitId", businessUnitId, DbType.Int32, ParameterDirection.Input);

    using IDbConnection cn = new SqlConnection(_sqlDataAccess.ConnectionString);
    return await cn.QueryAsync<Vendor, AppUser, Vendor>("dbo.SP_ZZZTest",
        (vendor, user) =>
        {
            if (vendor.CompanyUsers == null && user != null) { vendor.CompanyUsers = new(); };
            if (user != null) { vendor.CompanyUsers.Add(user); };
            return vendor;
        },
        param: p,
        splitOn: "Id",
        commandType: CommandType.StoredProcedure);
}
Run Code Online (Sandbox Code Playgroud)

这是我得到的结果: 衣冠楚楚

因此,Dapper 没有将用户映射到单个供应商下。而是将每个用户映射为用户列表,其中单个项目在 3 行上复制供应商的数据。

我做错了什么?

Ste*_*eve 5

是的,这是一个常见的“问题”。但一旦您了解了 lambda 调用背后的流程,解决起来就非常简单了。

lambda 表达式接收查询创建的三个记录,在调用 lambda 之前,Dapper 在splitOn配置点将每个记录一分为二。在此过程中,将为处理的每一行创建一个新的 Vendor 和新的 AppUser 实例。

因此,第二次/第三次调用时收到的 Vendor 实例与第一次/第二次调用时收到的 Vendor 实例不同。Dapper没有这种处理逻辑(我认为从性能角度避免它是正确的)。因此上面的代码将每个 AppUser 添加到 Vendor 的三个不同实例。

由您来“发现”供应商收到的包含与先前呼叫相同的数据。但如果有某种唯一的密钥来标识一个Vendor(记录的PK),这个问题就很容易解决

因此,这个“问题”可以使用字典来解决,其中键是供应商的 PK,并且您将 Dapper 传递的每个唯一供应商数据存储在该字典键下。然后您可以检查收到的 Vendor 数据是否已在字典中,并使用字典实例添加 AppUser。

Dictionary<int, Vendor> result = new Dictionary<int, Vendor>();

.....

using IDbConnection cn = new SqlConnection(_sqlDataAccess.ConnectionString);
_ = await cn.QueryAsync<Vendor, AppUser, Vendor>("dbo.SP_ZZZTest",
    (vendor, user) =>
    {
        if(!result.ContainsKey(vendor.vendorId))
        {
            vendor.CompanyUsers = new();
            result.Add(vendor.vendorId, vendor);
        }
        Vendor current = result[vendor.vendorId];
        if (user != null) 
            current.CompanyUsers.Add(user); 
            
        return vendor;
    },
    param: p,
    splitOn: "Id",
    commandType: CommandType.StoredProcedure);

// A VERY IMPORTANT POINT...
// You want to return the Vendors stored in the Values of the 
// Dictionary, not the Vendors returned by the QueryAsync call
return result.Values;
Run Code Online (Sandbox Code Playgroud)

  • 非常好的答案。我在其他地方看到了一个答案,其中不是“返回供应商;”,而是“返回 null;”。我不知道这是否会在内存方面产生影响,但它非常清楚,不使用 QueryAsync 的返回,字典的内容才是真正的输出。 (2认同)