web-dev-qa-db-ja.com

SqlDataReaderの結果をオブジェクトにマップする最速の方法

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キーワードを使用してnull許容列をキャストしましたが、正しいですか?
これは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());
                }
            }
        }
11
witoong623

ADO.NETコードを高速化する方法を次に示します。

選択を行うときは、select *を使用するのではなく、選択しているフィールドをリストしてください。これにより、データベースでフィールドの順序が変更された場合でも、フィールドが返される順序を確認できます。次に、これらのフィールドをリーダーから取得するときに、名前ではなくインデックスで取得します。インデックスの使用はより高速です。

また、強いビジネス上の理由がない限り、文字列データベースフィールドをnullにできないようにすることをお勧めします。次に、値がない場合は、空の文字列をデータベースに格納します。最後に、GetDataReaderメソッドを使用して、フィールドをそれらの型で取得し、コードでキャストする必要がないようにすることをお勧めします。したがって、たとえば、_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
Ron C

Dbやリフレクションについて疑わしいときは、「 Marc Gravell はどうしますか?」と自問します。

この場合、彼は FastMember !を使用します。そして、あなたもすべきです。これは Dapper でのデータ変換の基礎であり、独自のDataReaderをオブジェクトにマップするために簡単に使用できます(Dapperを使用したくない場合)。

以下は、SqlDataReaderT型に変換する拡張メソッドです。

注意:このコードは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;
}
16
robopim

pimbrouwer's 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; }
    }
}
5
HouseCat

私は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;
        }
    }
1
Hoang Minh

@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;
    }
0
Soenhay

これは他の回答に基づいていますが、インスタンス化するクラスのプロパティを読み取り、dataReaderから入力するために、標準リフレクションを使用しました。ディクショナリに保存されたb/w読み取りを使用してプロパティを保存することもできます。

名前をキーとして持つタイプのプロパティを含むディクショナリを初期化します。

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;

複数の型を扱う効率的​​なジェネリッククラスを作成する場合は、各ディクショナリをグローバルディクショナリに格納できます>。

0
pasx

このちょっとした作品

 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を使用していますが、概念は同じです。例として、私はこのように上記を呼び出すことによってゲームオブジェクトを埋めています-

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

クラスをdatareaderと100%一致させる必要があることに注意してください。フィールドをスキップするために、クエリを調整するか、ある種のリストを渡して渡します。 SQLDataReaderがSQL Server DBと通信することで、.netとデータベースの間で型の一致がかなり良くなります。 SQLiteでは、クラスのintをInt64として宣言して、これが機能し、文字列にnullが送信されるのを監視する必要があります。しかし、上記のコンセプトは機能するように見えるので、うまくいくはずです。これがOpが後にあったものだと思います。

0
infocyde

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

高度なマッピングにより、名前トランスフォーマーの使用、タイプの変更、フィールドの手動マッピング、またはオブジェクトのデータに関数を適用することもできるため、リーダーと異なるオブジェクトでも簡単にマッピングできます。

0
Greg

おそらく、私が提示するアプローチは最も効率的ではありませんが、コーディングの労力をほとんどかけずに作業を完了できます。ここで私が目にする主な利点は、互換性のある(マップ可能な)オブジェクトを作成する以外にデータ構造を処理する必要がないことです。

SqlDataReaderDataTableに変換する場合は、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();
0