【问题标题】:Custom mapping in DapperDapper 中的自定义映射
【发布时间】:2016-11-20 10:34:27
【问题描述】:

我正在尝试使用带有 Dapper 和多重映射的 CTE 来获取分页结果。我遇到了重复列的不便;例如,CTE 使我不必为列命名。

我想将以下查询映射到以下对象,而不是列名和属性之间的不匹配。

查询:

WITH TempSites AS(
    SELECT
        [S].[SiteID],
        [S].[Name] AS [SiteName],
        [S].[Description],
        [L].[LocationID],
        [L].[Name] AS [LocationName],
        [L].[Description] AS [LocationDescription],
        [L].[SiteID] AS [LocationSiteID],
        [L].[ReportingID]
    FROM (
        SELECT * FROM [dbo].[Sites] [1_S]
        WHERE [1_S].[StatusID] = 0
        ORDER BY [1_S].[Name]
        OFFSET 10 * (1 - 1) ROWS
        FETCH NEXT 10 ROWS ONLY
    ) S
        LEFT JOIN [dbo].[Locations] [L] ON [S].[SiteID] = [L].[SiteID]
),
MaxItems AS (SELECT COUNT(SiteID) AS MaxItems FROM Sites)

SELECT *
FROM TempSites, MaxItems

对象:

public class Site
{
    public int SiteID { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public List<Location> Locations { get; internal set; }
}

public class Location
{
    public int LocationID { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public Guid ReportingID { get; set; }
    public int SiteID { get; set; }
}

出于某种原因,我认为存在一个可以为我处理这种情况的命名约定,但我在文档中找不到它的提及。

【问题讨论】:

标签: c# .net sql-server dapper


【解决方案1】:

问题不止一个,让我们一一介绍。

CTE 列名重复:

CTE 不允许重复的列名,因此您必须使用别名来解析它们,最好使用一些命名约定,例如在您的查询尝试中。

出于某种原因,我认为存在一个可以为我处理这种情况的命名约定,但我在文档中找不到它的提及。

您可能想到将DefaultTypeMap.MatchNamesWithUnderscores 属性设置为true,但正如该属性的代码文档所述:

是否应该允许像 User_Id 这样的列名匹配像 UserId 这样的属性/字段?

显然这不是解决方案。但是这个问题可以很容易地通过引入自定义命名约定来解决,例如"{prefix}{propertyName}"(默认前缀是"{className}_")并通过Dapper的CustomPropertyTypeMap实现它。这是一个辅助方法:

public static class CustomNameMap
{
    public static void SetFor<T>(string prefix = null)
    {
        if (prefix == null) prefix = typeof(T).Name + "_";
        var typeMap = new CustomPropertyTypeMap(typeof(T), (type, name) =>
        {
            if (name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
                name = name.Substring(prefix.Length);
            return type.GetProperty(name);
        });
        SqlMapper.SetTypeMap(typeof(T), typeMap);
    }
}

现在您只需调用它(一次):

CustomNameMap.SetFor<Location>();

将命名约定应用于您的查询:

WITH TempSites AS(
    SELECT
        [S].[SiteID],
        [S].[Name],
        [S].[Description],
        [L].[LocationID],
        [L].[Name] AS [Location_Name],
        [L].[Description] AS [Location_Description],
        [L].[SiteID] AS [Location_SiteID],
        [L].[ReportingID]
    FROM (
        SELECT * FROM [dbo].[Sites] [1_S]
        WHERE [1_S].[StatusID] = 0
        ORDER BY [1_S].[Name]
        OFFSET 10 * (1 - 1) ROWS
        FETCH NEXT 10 ROWS ONLY
    ) S
        LEFT JOIN [dbo].[Locations] [L] ON [S].[SiteID] = [L].[SiteID]
),
MaxItems AS (SELECT COUNT(SiteID) AS MaxItems FROM Sites)

SELECT *
FROM TempSites, MaxItems

你已经完成了那部分。当然,如果您愿意,您可以使用较短的前缀,例如“Loc_”。

将查询结果映射到提供的类:

在这种特殊情况下,您需要使用Query 方法重载,该方法允许您传递Func&lt;TFirst, TSecond, TReturn&gt; map 委托并统一splitOn 参数以将LocationID 指定为拆分列。然而,这还不够。 Dapper 的 Multi Mapping 功能允许您在需要 SiteLocation list 时将单行拆分为多个 single 对象(如 LINQ Join) (如 LINQ GroupJoin)。

这可以通过使用Query 方法投影到一个临时匿名类型,然后使用常规 LINQ 生成所需的输出来实现,如下所示:

var sites = cn.Query(sql, (Site site, Location loc) => new { site, loc }, splitOn: "LocationID")
    .GroupBy(e => e.site.SiteID)
    .Select(g =>
    {
        var site = g.First().site;
        site.Locations = g.Select(e => e.loc).Where(loc => loc != null).ToList();
        return site;
    })
    .ToList();

其中cn 被打开SqlConnectionsql 是一个string 持有上述查询。

【讨论】:

    【解决方案2】:

    您可以使用ColumnAttributeTypeMapper 将列名映射到另一个属性。

    有关详细信息,请参阅我对 Gist 的第一条评论。

    您可以像这样进行映射

    public class Site
    {
        public int SiteID { get; set; }
        [Column("SiteName")]
        public string Name { get; set; }
        public string Description { get; set; }
        public List<Location> Locations { get; internal set; }
    }
    
    public class Location
    {
        public int LocationID { get; set; }
        [Column("LocationName")]
        public string Name { get; set; }
        [Column("LocationDescription")]
        public string Description { get; set; }
        public Guid ReportingID { get; set; }
        [Column("LocationSiteID")]
        public int SiteID { get; set; }
    }
    

    可以使用以下 3 种方法之一来完成映射

    方法一

    手动为您的模型设置自定义类型映射器一次:

    Dapper.SqlMapper.SetTypeMap(typeof(Site), new ColumnAttributeTypeMapper<Site>());
    Dapper.SqlMapper.SetTypeMap(typeof(Location), new ColumnAttributeTypeMapper<Location>());
    

    方法二

    对于 .NET Framework >= v4.0 的类库,您可以使用 PreApplicationStartMethod 注册您的类以进行自定义类型映射。

    using System.Web;
    using Dapper;
    
    [assembly: PreApplicationStartMethod(typeof(YourNamespace.Initiator), "RegisterModels")]
    
    namespace YourNamespace
    {
        public class Initiator
        {
            private static void RegisterModels()
            {
                 SqlMapper.SetTypeMap(typeof(Site), new ColumnAttributeTypeMapper<Site>());
                 SqlMapper.SetTypeMap(typeof(Location), new ColumnAttributeTypeMapper<Location>());
                 // ...
            }
        }
    }
    

    方法3

    或者您可以通过反射和设置类型映射找到应用 ColumnAttribute 的类。这可能会慢一点,但它会自动为您完成程序集中的所有映射。加载程序集后,只需调用 RegisterTypeMaps()

        public static void RegisterTypeMaps()
        {
            var mappedTypes = Assembly.GetAssembly(typeof (Initiator)).GetTypes().Where(
                f =>
                f.GetProperties().Any(
                    p =>
                    p.GetCustomAttributes(false).Any(
                        a => a.GetType().Name == ColumnAttributeTypeMapper<dynamic>.ColumnAttributeName)));
    
            var mapper = typeof(ColumnAttributeTypeMapper<>);
            foreach (var mappedType in mappedTypes)
            {
                var genericType = mapper.MakeGenericType(new[] { mappedType });
                SqlMapper.SetTypeMap(mappedType, Activator.CreateInstance(genericType) as SqlMapper.ITypeMap);
            }
        }
    

    【讨论】:

    • 我正在尝试方法 1,但我得到 错误 CS0535:'FallbackTypeMapper' 没有实现接口成员 'SqlMapper.ITypeMap.FindExplicitConstructor()'。有什么建议吗?
    • 对于未来的旅行者,在这个项目中搜索FallbackTypeMapper:gist.github.com/senjacob/8539127
    • @CarvellWakeman 这是我的要点的相同链接,在这个答案的第一行给出。
    【解决方案3】:

    下面的代码应该可以很好地让您加载具有相关位置的网站列表

    var conString="your database connection string here";
    using (var conn =   new SqlConnection(conString))
    {
        conn.Open();
        string qry = "SELECT S.SiteId, S.Name, S.Description, L.LocationId,  L.Name,L.Description,
                      L.ReportingId
                      from Site S  INNER JOIN   
                      Location L ON S.SiteId=L.SiteId";
        var sites = conn.Query<Site, Location, Site>
                         (qry, (site, loc) => { site.Locations = loc; return site; });
        var siteCount = sites.Count();
        foreach (Site site in sites)
        {
            //do something
        }
        conn.Close(); 
    }
    

    【讨论】:

    • 感谢您的回答,但您对查询所做的更改完全忽略了分页的需要以及返回可用项目的最大数量的要求。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-03-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-02-17
    相关资源
    最近更新 更多