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

时间:2016-12-08 13:02:14

标签: c# ado.net dapper

我正在比较Dapper与ADO.NET和Dapper之间的物化时间。 最终,Dapper往往比ADO.NET更快,虽然第一次执行给定的获取查询比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());
                }
            }
        }

12 个答案:

答案 0 :(得分:16)

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

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

以下是将<div class="container"> <div class="jumbotron"> <form method="POST"> <div class="text-left">{% csrf_token %} {{ form.as_p }} {{ formset.management_form }} {{ formset }} </div> <button type="submit" class="btn btn-primary btn-outline">submit</button> </form> </div> 转换为SqlDataReader类型的扩展方法:

  

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

T

答案 1 :(得分:4)

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; }
    }
}

答案 2 :(得分:2)

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

进行选择时,列出您选择的字段,而不是使用select *。这样,即使数据库中的顺序发生变化,也可以确保字段返回的顺序。然后从Reader中获取这些字段时,按索引而不是按名称获取。使用和索引更快。

此外,我建议不要将字符串数据库字段置为可空,除非有强烈的商业原因。如果没有值,则只在数据库中存储空字符串。最后,我建议您使用Get上的DataReader方法来获取您所在类型的字段,以便在代码中不需要进行投射。因此,例如,不要将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++);

给它一个旋转,看看它现在为你做了。

答案 3 :(得分:1)

我同时回答了小问题和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;
        }
    }

答案 4 :(得分:1)

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);
}

答案 5 :(得分:0)

NuGet中有一个SqlDataReader映射器库,可帮助您将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();

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

答案 6 :(得分:0)

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

如果将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();

答案 7 :(得分:0)

这是基于其他答案的,但是我使用标准反射来读取要实例化的类的属性,并从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 :(得分:0)

将@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://stackoverflow.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<T>或结果中的多个表。

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

    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))
        {
            conn.Open();
            SqlCommand sqlCommand = GetSqlCommand(conn, storedProcedureName, sqlParameters);
            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;
    }

答案 9 :(得分:0)

这有点用

 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;

请注意,您必须让您的课程与数据阅读器100%匹配,因此请调整查询以适合或传递某种列表以跳过字段。通过将SQLDataReader与SQL Server DB进行通信,您可以在.net和数据库之间实现非常好的类型匹配。使用SQLite,您必须在您的类中将int声明为Int64s才能起作用,并注意向字符串发送空值。但是上述概念似乎行得通,因此应该可以解决。我认为这就是Op的追求。

答案 10 :(得分:0)

我喜欢最受好评的答案提到@MarkGravel和他的FastMember。 但是,如果您已经在使用Dapper,这也是他的组成部分,则可以使用Dapper的GetRowParser,如下所示:

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

while (reader.Read())
{
    var myObject = parser(reader);
}

答案 11 :(得分:0)

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

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

class EmployeeDao
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int? Age { get; set; }
}

要进行自动映射,您可以调用扩展方法 MapToObject<T>()

var reader = await sqlCmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
    var employeeObj = reader.MapToObject<EmployeeDao>();
}

你将摆脱几十行不可读和难以维护的代码。

此处的分步示例:https://github.com/LucaMozzo/DbDataReaderMapper