将 SqlDataReader 的结果映射到对象的最快方法

Posted

技术标签:

【中文标题】将 SqlDataReader 的结果映射到对象的最快方法【英文标题】:Fastest way to map result of SqlDataReader to object 【发布时间】:2017-04-23 17:42:52 【问题描述】:

我正在比较 Dapper 与 ADO.NET 和 Dapper 之间的实现时间。 最终,Dapper 往往比 ADO.NET 快,尽管第一次执行给定的 fetch 查询比 ADO.NET 慢。 一些结果表明 Dapper 比 ADO.NET 快一点(几乎所有结果都表明它具有可比性) 所以我认为我使用低效的方法将 SqlDataReader 的结果映射到对象。 这是我的代码

var sql = "SELECT * FROM Sales.SalesOrderHeader WHERE SalesOrderID = @Id";
        var conn = new SqlConnection(ConnectionString);
        var stopWatch = new Stopwatch();

        try
        
            conn.Open();
            var sqlCmd = new SqlCommand(sql, conn);

            for (var i = 0; i < keys.GetLength(0); i++)
            
                for (var r = 0; r < keys.GetLength(1); r++)
                
                    stopWatch.Restart();
                    sqlCmd.Parameters.Clear();
                    sqlCmd.Parameters.AddWithValue("@Id", keys[i, r]);
                    var reader = await sqlCmd.ExecuteReaderAsync();
                    SalesOrderHeaderSQLserver salesOrderHeader = null;

                    while (await reader.ReadAsync())
                    
                        salesOrderHeader = new SalesOrderHeaderSQLserver();
                        salesOrderHeader.SalesOrderId = (int)reader["SalesOrderId"];
                        salesOrderHeader.SalesOrderNumber = reader["SalesOrderNumber"] as string;
                        salesOrderHeader.AccountNumber = reader["AccountNumber"] as string;
                        salesOrderHeader.BillToAddressID = (int)reader["BillToAddressID"];
                        salesOrderHeader.TotalDue = (decimal)reader["TotalDue"];
                        salesOrderHeader.Comment = reader["Comment"] as string;
                        salesOrderHeader.DueDate = (DateTime)reader["DueDate"];
                        salesOrderHeader.CurrencyRateID = reader["CurrencyRateID"] as int?;
                        salesOrderHeader.CustomerID = (int)reader["CustomerID"];
                        salesOrderHeader.SalesPersonID = reader["SalesPersonID"] as int?;
                        salesOrderHeader.CreditCardApprovalCode = reader["CreditCardApprovalCode"] as string;
                        salesOrderHeader.ShipDate = reader["ShipDate"] as DateTime?;
                        salesOrderHeader.Freight = (decimal)reader["Freight"];
                        salesOrderHeader.ModifiedDate = (DateTime)reader["ModifiedDate"];
                        salesOrderHeader.OrderDate = (DateTime)reader["OrderDate"];
                        salesOrderHeader.TerritoryID = reader["TerritoryID"] as int?;
                        salesOrderHeader.CreditCardID = reader["CreditCardID"] as int?;
                        salesOrderHeader.OnlineOrderFlag = (bool)reader["OnlineOrderFlag"];
                        salesOrderHeader.PurchaseOrderNumber = reader["PurchaseOrderNumber"] as string;
                        salesOrderHeader.RevisionNumber = (byte)reader["RevisionNumber"];
                        salesOrderHeader.Rowguid = (Guid)reader["Rowguid"];
                        salesOrderHeader.ShipMethodID = (int)reader["ShipMethodID"];
                        salesOrderHeader.ShipToAddressID = (int)reader["ShipToAddressID"];
                        salesOrderHeader.Status = (byte)reader["Status"];
                        salesOrderHeader.SubTotal = (decimal)reader["SubTotal"];
                        salesOrderHeader.TaxAmt = (decimal)reader["TaxAmt"];
                    

                    stopWatch.Stop();
                    reader.Close();
                    await PrintTestFindByPKReport(stopWatch.ElapsedMilliseconds, salesOrderHeader.SalesOrderId.ToString());
                

我使用as 关键字强制转换为可空列,对吗? 这是 Dapper 的代码。

using (var conn = new SqlConnection(ConnectionString))
        
            conn.Open();
            var stopWatch = new Stopwatch();

            for (var i = 0; i < keys.GetLength(0); i++)
            
                for (var r = 0; r < keys.GetLength(1); r++)
                
                    stopWatch.Restart();
                    var result = (await conn.QueryAsync<SalesOrderHeader>("SELECT * FROM Sales.SalesOrderHeader WHERE SalesOrderID = @Id", new  Id = keys[i, r] )).FirstOrDefault();
                    stopWatch.Stop();
                    await PrintTestFindByPKReport(stopWatch.ElapsedMilliseconds, result.ToString());
                
            
        

【问题讨论】:

如果你删除异步代码并进行比较会怎样? “最终,Dapper 往往比 ADO.NET 更快” - 句子不计算; Dapper 位于 ADO.NET 之上;它不能比它消耗的东西,而且ADO.NET 不提供您所追求的服务...您能更具体地说明您的意思吗? 在我看来,您的主要问题是您正在执行大量查询;您是否考虑过使用 INNER JOIN 或多个 SELECT (.QueryMultiple) 在 一个 查询中执行所有操作,而不是执行多个查询? @MarcGravell 当我问这个问题时,我实际上忘记了 Dapper 位于 ADO.NET 之上。在我正确比较了所有测试结果之后(不仅仅是凝视它)。我必须承认我夸大了,一些结果表明 Dapper 更快的最高时间差是 9 毫秒,几乎所有的结果都表明它具有可比性,很少有表明 ADO.NET 更快。真的很抱歉浪费了您的时间。 【参考方案1】:

这是一种让您的 ADO.NET 代码更快的方法。

当您进行选择时,请列出您选择的字段,而不是使用 select *。这将让您确保字段返回的顺序,即使该顺序在数据库中发生更改。然后从阅读器获取这些字段时,通过索引而不是名称获取它们。使用和索引更快。

另外,我建议不要将字符串数据库字段设置为可空,除非有很强的商业原因。如果没有值,则只需在数据库中存储一个空白字符串。最后,我建议在DataReader 上使用Get 方法来获取您的字段类型,这样您的代码中就不需要强制转换。因此,例如,不要将 DataReader[index++] 值转换为 int 使用 DataReader.GetInt(index++)

例如,这段代码:

 salesOrderHeader = new SalesOrderHeaderSQLserver();
 salesOrderHeader.SalesOrderId = (int)reader["SalesOrderId"];
 salesOrderHeader.SalesOrderNumber =       reader["SalesOrderNumber"] as string;
 salesOrderHeader.AccountNumber = reader["AccountNumber"] as string;

变成

 int index = 0;
 salesOrderHeader = new SalesOrderHeaderSQLserver();
 salesOrderHeader.SalesOrderId = reader.GetInt(index++);
 salesOrderHeader.SalesOrderNumber = reader.GetString(index++);
 salesOrderHeader.AccountNumber = reader.GetString(index++);

试一试,看看它对你有什么作用。

【讨论】:

谢谢,虽然我懒得列出所有的列名并使用GetXXX 占据位置XD 的方法,但当我将它映射到属性时很容易出错。但是,如果它更快,我会尝试。你能解释一下字符串字段不应该为空吗? 也许只有在需要高性能代码的地方才值得这样做。创建一个可以为你编写这段代码块的代码生成器是解决惰性问题的一种方法,但可惜这也需要工作:-) 只报告测试结果,按列位置获取值比列名快,但在我的情况下,它快 1 毫秒。但事实证明,ADO.NET 并不比 Dapper 慢,而且 Dapper 在性能上与 ADO.NET 不相上下。【参考方案2】:

当对任何 db 或反射有疑问时,我会问自己,“Marc Gravell 会做什么?”。

在这种情况下,他会使用FastMember!你也应该这样做。它是Dapper 中数据转换的基础,可以轻松用于将您自己的 DataReader 映射到对象(如果您不想使用 Dapper)。

以下是将SqlDataReader 转换为T 类型的扩展方法:

请注意:此代码暗示了对 FastMember 的依赖,并且是为 .NET Core 编写的(尽管可以很容易地转换为 .NET Framework/Standard 兼容代码)。

public static T ConvertToObject<T>(this SqlDataReader rd) where T : class, new()

    Type type = typeof(T);
    var accessor = TypeAccessor.Create(type);
    var members = accessor.GetMembers();
    var t = new T();

    for (int i = 0; i < rd.FieldCount; i++)
    
        if (!rd.IsDBNull(i))
        
            string fieldName = rd.GetName(i);

            if (members.Any(m => string.Equals(m.Name, fieldName, StringComparison.OrdinalIgnoreCase)))
            
                accessor[t, fieldName] = rd.GetValue(i);
            
        
    

    return t;

【讨论】:

与反射相比,它在我的测试中要慢得多。如果您缓存成员,您可能会在第二次获得更快的结果。 @manit 当然你应该缓存访问器,否则你每次都编译并发出访问器。 Fastmember 在内部执行此操作。但是,是的,您当然也可以这样做。这个答案并不意味着作为最终解决方案。而是整体设计中的一个组成部分。【参考方案3】:

借鉴pimbrouwers' answer的方法,稍微优化一下。减少 LINQ 调用。

仅映射对象和数据字段名称中的属性。处理 DBNull。其他假设是您的域模型属性绝对等于表列/字段名称。

/// <summary>
/// Maps a SqlDataReader record to an object.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="dataReader"></param>
/// <param name="newObject"></param>
public static void MapDataToObject<T>(this SqlDataReader dataReader, T newObject)

    if (newObject == null) throw new ArgumentNullException(nameof(newObject));

    // Fast Member Usage
    var objectMemberAccessor = TypeAccessor.Create(newObject.GetType());
    var propertiesHashSet =
            objectMemberAccessor
            .GetMembers()
            .Select(mp => mp.Name)
            .ToHashSet();

    for (int i = 0; i < dataReader.FieldCount; i++)
    
        if (propertiesHashSet.Contains(dataReader.GetName(i)))
        
            objectMemberAccessor[newObject, dataReader.GetName(i)]
                = dataReader.IsDBNull(i) ? null : dataReader.GetValue(i);
        
    

示例用法:

public async Task<T> GetAsync<T>(string storedProcedureName, SqlParameter[] sqlParameters = null) where T : class, new()

    using (var conn = new SqlConnection(_connString))
    
        var sqlCommand = await GetSqlCommandAsync(storedProcedureName, conn, sqlParameters);
        var dataReader = await sqlCommand.ExecuteReaderAsync(CommandBehavior.CloseConnection);

        if (dataReader.HasRows)
        
            var newObject = new T();

            if (await dataReader.ReadAsync())
             dataReader.MapDataToObject(newObject); 

            return newObject;
        
        else
         return null; 
    

【讨论】:

这个版本区分大小写,Oracle总是返回大写的列名 没错。属性/列必须匹配并且区分大小写。但是,您可以将 objectMemberAccessor 键修改为 ToUpper() / ToLower() 的字符串函数。如果我曾经做过一个 Attributes 版本,我会修改这个解决方案。【参考方案4】:

NuGet 中有一个SqlDataReader Mapper 库,可帮助您将 SqlDataReader 映射到对象。以下是它的使用方法(来自 GitHub 文档):

var mappedObject = new SqlDataReaderMapper<DTOObject>(reader)
    .Build();

或者,如果您想要更高级的映射:

var mappedObject = new SqlDataReaderMapper<DTOObject>(reader)
     .NameTransformers("_", "")
     .ForMember<int>("CurrencyId")
     .ForMember("CurrencyCode", "Code")
     .ForMember<string>("CreatedByUser", "User").Trim()
     .ForMemberManual("CountryCode", val => val.ToString().Substring(0, 10))
     .ForMemberManual("ZipCode", val => val.ToString().Substring(0, 5), "ZIP")
     .Build();

高级映射允许您使用名称转换器、更改类型、手动映射字段,甚至将函数应用于对象的数据,这样即使对象与阅读器不同,您也可以轻松映射对象。

【讨论】:

这对我来说比其他解决方案慢 20 倍。 :( @SunnyPatel 如果您使用这个库的 1.0.2 版本(实际上依赖于 FastMember),那么它的执行速度将与上述解决方案一样快。【参考方案5】:

我接受了 pimbrouwers 和 HouseCat 的回答并提出了我。在我的场景中,数据库中的列名具有蛇形大小写格式。

public static T ConvertToObject<T>(string query) where T : class, new()
    
        using (var conn = new SqlConnection(AutoConfig.ConnectionString))
        
            conn.Open();
            var cmd = new SqlCommand(query) Connection = conn;
            var rd = cmd.ExecuteReader();
            var mappedObject = new T();

            if (!rd.HasRows) return mappedObject;
            var accessor = TypeAccessor.Create(typeof(T));
            var members = accessor.GetMembers();
            if (!rd.Read()) return mappedObject;
            for (var i = 0; i < rd.FieldCount; i++)
            
                var columnNameFromDataTable = rd.GetName(i);
                var columnValueFromDataTable = rd.GetValue(i);

                var splits = columnNameFromDataTable.Split('_');
                var columnName = new StringBuilder("");
                foreach (var split in splits)
                
                    columnName.Append(CultureInfo.InvariantCulture.TextInfo.ToTitleCase(split.ToLower()));
                

                var mappedColumnName = members.FirstOrDefault(x =>
                    string.Equals(x.Name, columnName.ToString(), StringComparison.OrdinalIgnoreCase));

                if(mappedColumnName == null) continue;
                var columnType = mappedColumnName.Type;

                if (columnValueFromDataTable != DBNull.Value)
                
                    accessor[mappedObject, columnName.ToString()] = Convert.ChangeType(columnValueFromDataTable, columnType);
                
            

            return mappedObject;
        
    

【讨论】:

【参考方案6】:

也许我将介绍的方法不是最有效的,但只需很少的编码工作即可完成工作。我在这里看到的主要好处是,除了构建兼容(可映射)对象之外,您不必处理数据结构。

如果您将SqlDataReader 转换为DataTable,然后使用JsonConvert.SerializeObject 对其进行序列化,您可以使用JsonConvert.DeserializeObject 将其反序列化为已知的对象类型

这是一个实现示例:

        SqlDataReader reader = null;
        SqlConnection myConnection = new SqlConnection();
        myConnection.ConnectionString = ConfigurationManager.ConnectionStrings["DatabaseConnection"].ConnectionString;
        SqlCommand sqlCmd = new SqlCommand();
        sqlCmd.CommandType = CommandType.Text;
        sqlCmd.CommandText = "SELECT * FROM MyTable";
        sqlCmd.Connection = myConnection;
        myConnection.Open();
        reader = sqlCmd.ExecuteReader();

        var dataTable = new DataTable();
        dataTable.Load(reader);

        List<MyObject> myObjects = new List<MyObject>();

        if (dataTable.Rows.Count > 0)
        
            var serializedMyObjects = JsonConvert.SerializeObject(dataTable);
            // Here you get the object
            myObjects = (List<MyObject>)JsonConvert.DeserializeObject(serializedMyObjects, typeof(List<MyObject>));
        

        myConnection.Close();

【讨论】:

棘手!我喜欢它。 DataTable 比 DataReader 慢。如果您在应用程序中使用 DataTable 作为数据模型,那么它是更快的方法。【参考方案7】:

这是基于其他答案,但我使用标准反射来读取您要实例化的类的属性并从 dataReader 填充它。您还可以使用字典持久化黑白读取来存储属性。

初始化一个字典,其中包含类型中的属性,并将它们的名称作为键。

var type = typeof(Foo);
var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
var propertyDictionary = new Dictionary<string,PropertyInfo>();
foreach(var property in properties)

    if (!property.CanWrite) continue;
    propertyDictionary.Add(property.Name, property);

从 DataReader 设置类型的新实例的方法如下:

var foo = new Foo();
//retrieve the propertyDictionary for the type
for (var i = 0; i < dataReader.FieldCount; i++)

    var n = dataReader.GetName(i);
    PropertyInfo prop;
    if (!propertyDictionary.TryGetValue(n, out prop)) continue;
    var val = dataReader.IsDBNull(i) ? null : dataReader.GetValue(i);
    prop.SetValue(foo, val, null);

return foo;

如果您想编写一个高效的泛型类来处理多种类型,您可以将每个字典存储在一个全局字典中>。

【讨论】:

【参考方案8】:

将@HouseCat 的solution 修改为不区分大小写:

    /// <summary>
    /// Maps a SqlDataReader record to an object. Ignoring case.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="dataReader"></param>
    /// <param name="newObject"></param>
    /// <remarks>https://***.com/a/52918088</remarks>
    public static void MapDataToObject<T>(this SqlDataReader dataReader, T newObject)
    
        if (newObject == null) throw new ArgumentNullException(nameof(newObject));

        // Fast Member Usage
        var objectMemberAccessor = TypeAccessor.Create(newObject.GetType());
        var propertiesHashSet =
                objectMemberAccessor
                .GetMembers()
                .Select(mp => mp.Name)
                .ToHashSet(StringComparer.InvariantCultureIgnoreCase);

        for (int i = 0; i < dataReader.FieldCount; i++)
        
            var name = propertiesHashSet.FirstOrDefault(a => a.Equals(dataReader.GetName(i), StringComparison.InvariantCultureIgnoreCase));
            if (!String.IsNullOrEmpty(name))
            
                objectMemberAccessor[newObject, name]
                    = dataReader.IsDBNull(i) ? null : dataReader.GetValue(i);
            
        
    

编辑:这不适用于List&lt;T&gt; 或结果中的多个表。

EDIT2:将调用函数更改为此适用于列表。无论如何,如果我期待一个对象,我将返回一个对象列表并获取第一个索引。我还没有查看多个表,但我会的。

    public static void MapDataToObject<T>(this SqlDataReader dataReader, T newObject)
    
        if (newObject == null) throw new ArgumentNullException(nameof(newObject));

        // Fast Member Usage
        var objectMemberAccessor = TypeAccessor.Create(newObject.GetType());
        var propertiesHashSet =
                objectMemberAccessor
                .GetMembers()
                .Select(mp => mp.Name)
                .ToHashSet(StringComparer.InvariantCultureIgnoreCase);

        for (int i = 0; i < dataReader.FieldCount; i++)
        
            var name = propertiesHashSet.FirstOrDefault(a => a.Equals(dataReader.GetName(i), StringComparison.InvariantCultureIgnoreCase));
            if (!String.IsNullOrEmpty(name))
            
                //Attention! if you are getting errors here, then double check that your model and sql have matching types for the field name.
                //Check api.log for error message!
                objectMemberAccessor[newObject, name]
                    = dataReader.IsDBNull(i) ? null : dataReader.GetValue(i);
            
        
    

编辑 3:更新以显示示例调用函数。

    public async Task<List<T>> ExecuteReaderAsync<T>(string storedProcedureName, SqlParameter[] sqlParameters = null) where T : class, new()
    
        var newListObject = new List<T>();

        using (var conn = new SqlConnection(_connectionString))
        
            using (SqlCommand sqlCommand = GetSqlCommand(conn, storedProcedureName, sqlParameters))
            
                await conn.OpenAsync();
                using (var dataReader = await sqlCommand.ExecuteReaderAsync(CommandBehavior.Default))
                
                    if (dataReader.HasRows)
                    
                        while (await dataReader.ReadAsync())
                        
                            var newObject = new T();
                            dataReader.MapDataToObject(newObject);
                            newListObject.Add(newObject);
                        
                    
                
            
        

        return newListObject;
    

【讨论】:

不错,虚拟高五! @HouseCat 谢谢,我已经在多个 API 上使用此功能近 2 年了。我唯一遇到的问题是,在添加新对象时,DB 和模型之间偶尔会出现类型不匹配,我用一个花哨的评论来缓解这种情况。【参考方案9】:

这个有点用

 public static object PopulateClass(object o, SQLiteDataReader dr, Type T)
    
        Type type = o.GetType();
        PropertyInfo[] properties = type.GetProperties();

        foreach (PropertyInfo property in properties)
        
            T.GetProperty(property.Name).SetValue(o, dr[property.Name],null);
        
        return o;
    

注意我在这里使用的是 SQlite,但概念是一样的。作为一个例子,我通过像这样调用上面的方法来填充一个 Game 对象-

g = PopulateClass(g, dr, typeof(Game)) as Game;

请注意,您必须让您的班级与 datareader 100% 匹配,因此请调整您的查询以适应或传递某种列表以跳过字段。通过 SQLDataReader 与 SQL Server DB 对话,您可以在 .net 和数据库之间进行很好的类型匹配。使用 SQLite,您必须在您的类中将您的 int 声明为 Int64s 才能正常工作并观察将空值发送到字符串。但上述概念似乎有效,所以它应该让你继续前进。我认为这就是 Op 所追求的。

【讨论】:

【参考方案10】:

投票最多的答案提到了@MarkGravel 和他的 FastMember。 但是如果你已经在使用Dapper,这也是他的一个组件,你可以像这样使用Dapper的GetRowParser

var parser = reader.GetRowParser<MyObject>(typeof(MyObject));

while (reader.Read())

    var myObject = parser(reader);

【讨论】:

更容易处理 >1 行:conn.Query(sql) reader.GetRowParser(Type) 非常适合在您不知道类型(泛型/反射)时读取多个对象 是否有可能每个类只解析一次并缓存内部实现? @MaulikModi 这就是 Dapper 在内部的工作方式,它缓存预编译的表达式【参考方案11】:
List<T> result = new List<T>();
SqlDataReader reader = com.ExecuteReader();

while(reader.Read())
                        
    Type type = typeof(T);
    T obj = (T)Activator.CreateInstance(type);
    PropertyInfo[] properties = type.GetProperties();

    foreach (PropertyInfo property in properties)
    
        try
        
            var value = reader[property.Name];
            if (value != null)
                property.SetValue(obj, Convert.ChangeType(value.ToString(), property.PropertyType));
        
        catch                            
    
    result.Add(obj);

【讨论】:

【参考方案12】:

您可以使用命令Install-Package DbDataReaderMapper 或使用IDE 的包管理器安装包DbDataReaderMapper

然后您可以创建您的数据访问对象(我将选择一个比您提供的更短的示例):

class EmployeeDao

    public int Id  get; set; 
    public string FirstName  get; set; 
    public string LastName  get; set; 
    public int? Age  get; set; 

要进行自动映射,您可以调用扩展方法MapToObject&lt;T&gt;()

var reader = await sqlCmd.ExecuteReaderAsync();
while (await reader.ReadAsync())

    var employeeObj = reader.MapToObject<EmployeeDao>();

您将摆脱数十行不可读且难以维护的代码。

这里的分步示例:https://github.com/LucaMozzo/DbDataReaderMapper

【讨论】:

以上是关于将 SqlDataReader 的结果映射到对象的最快方法的主要内容,如果未能解决你的问题,请参考以下文章

实体框架数据集映射

.NET SqlDataReader对象是否使用数据库游标,或者整个结果集是否已加载到RAM中?

使用表达式目录树实现SqlDataReader到实体的映射

SqlDataReader 参数不起作用

如何将 spark sql 查询结果映射到对象?

如何将结果集映射到对象的嵌套结构?