web-dev-qa-db-fra.com

Moyen le plus rapide de mapper le résultat de SqlDataReader vers l'objet

Je compare le temps de matérialisation entre Dapper et ADO.NET et Dapper. En fin de compte, Dapper a tendance à être plus rapide qu'ADO.NET, bien que la première fois qu'une requête d'extraction donnée ait été exécutée soit plus lente qu'ADO.NET. quelques résultats montrent que Dapper est un peu plus rapide qu'ADO.NET (presque tous les résultats montrent qu'il est comparable cependant)
Je pense donc que j'utilise une approche inefficace pour mapper le résultat de SqlDataReader à l'objet.
Voici mon code

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

J'ai utilisé le mot clé as pour transtyper dans la colonne nullable, est-ce correct?
et voici le code de 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

Voici un moyen de rendre votre code ADO.NET plus rapide.

Lorsque vous effectuez votre sélection, répertoriez les champs que vous sélectionnez plutôt que d'utiliser select *. Cela vous permettra de garantir l'ordre de retour des champs même si cet ordre change dans la base de données. Ensuite, lorsque vous obtenez ces champs du Reader, récupérez-les par index plutôt que par nom. L'utilisation et l'indexation sont plus rapides.

En outre, je vous recommande de ne pas rendre les champs de base de données de chaînes nullables sauf s'il existe une raison commerciale solide. Ensuite, stockez simplement une chaîne vide dans la base de données s'il n'y a pas de valeur. Enfin, je recommanderais d'utiliser les méthodes Get sur le DataReader pour obtenir vos champs dans le type qu'ils sont afin que la conversion ne soit pas nécessaire dans votre code. Ainsi, par exemple, au lieu de transtyper la valeur DataReader[index++] En un entier, utilisez DataReader.GetInt(index++)

Ainsi, par exemple, ce code:

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

devient

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

Donnez-lui un tourbillon et voyez maintenant qu'il le fait pour vous.

3
Ron C

En cas de doute sur quoi que ce soit db ou réflexion, je me demande, "que ferait Marc Gravell ?".

Dans ce cas, il utiliserait FastMember ! Et vous devriez aussi. C'est le fondement des conversions de données dans Dapper , et peut facilement être utilisé pour mapper votre propre DataReader à un objet (si vous ne souhaitez pas utiliser Dapper).

Voici une méthode d'extension convertissant un SqlDataReader en quelque chose de type T:

S'IL VOUS PLAÎT NOTE: Ce code implique une dépendance à FastMember et est écrit pour .NET Core (bien qu'il puisse facilement être converti en .NET Framework/Standard conforme code).

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

A pris la méthode de réponse de pimbrouwers et l'a légèrement optimisée. Réduisez les appels LINQ.

Mappe uniquement les propriétés trouvées dans les noms d'objet et de champ de données. Poignées DBNull. Une autre hypothèse est que les propriétés de votre modèle de domaine sont absolument égales aux noms de colonne/champ de table.

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

Exemple d'utilisation:

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

J'ai pris les deux pimbrouwers et les réponses de HouseCat et suis venu avec moi. Dans mon scénario, le nom de colonne dans la base de données a le format de cas de serpent.

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

Modifié @ HouseCat solution pour être insensible à la casse:

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

EDIT: cela ne fonctionne pas pour List<T> ou plusieurs tableaux dans les résultats.

EDIT2: Changer la fonction d'appel à cela fonctionne pour les listes. Je vais juste retourner une liste d'objets, peu importe quoi, et obtenir le premier index si je m'attendais à un seul objet. Je n'ai pas encore examiné plusieurs tables mais je le ferai.

    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

Ceci est basé sur les autres réponses mais j'ai utilisé la réflexion standard pour lire les propriétés de la classe que vous souhaitez instancier et la remplir à partir du dataReader. Vous pouvez également stocker les propriétés à l'aide d'un dictionnaire de lectures n/b persistantes.

Initialisez un dictionnaire contenant les propriétés du type avec leurs noms comme clés.

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

La méthode pour définir une nouvelle instance du type à partir du DataReader serait comme:

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;

Si vous souhaitez écrire une classe générique efficace traitant de plusieurs types, vous pouvez stocker chaque dictionnaire dans un dictionnaire global>.

0
pasx

Cela fonctionne un peu

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

Notez que j'utilise SQlite ici mais le concept est le même. Par exemple, je remplis un objet Game en appelant ce qui précède comme ceci-

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

Notez que vous devez faire correspondre votre classe avec le datareader à 100%, alors ajustez votre requête en fonction ou passez une sorte de liste pour ignorer les champs. Avec un SQLDataReader parlant à une base de données SQL Server, vous avez une très bonne correspondance de type entre .net et la base de données. Avec SQLite, vous devez déclarer vos entrées dans votre classe en tant qu'Int64 pour que cela fonctionne et regarder l'envoi de null aux chaînes. Mais le concept ci-dessus semble fonctionner, il devrait donc vous permettre de continuer. Je pense que c'est ce que le Op était après.

0
infocyde

Il existe une bibliothèque SqlDataReader Mapper dans NuGet qui vous aide à mapper SqlDataReader à un objet. Voici comment il peut être utilisé (à partir de la documentation GitHub):

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

Ou, si vous souhaitez une cartographie plus avancée:

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

Le mappage avancé vous permet d'utiliser des transformateurs de noms, de modifier les types, de mapper des champs manuellement ou même d'appliquer des fonctions aux données de l'objet afin que vous puissiez facilement mapper des objets même s'ils diffèrent d'un lecteur.

0
Greg

L'approche que je présenterai n'est peut-être pas la plus efficace mais fait le travail avec très peu d'effort de codage. Le principal avantage que je vois ici est que vous n'avez pas à gérer une structure de données autre que la construction d'un objet compatible (mappable).

Si vous convertissez SqlDataReader en DataTable, puis sérialisez-le à l'aide de JsonConvert.SerializeObject vous pouvez ensuite le désérialiser en un type d'objet connu en utilisant JsonConvert.DeserializeObject

Voici un exemple d'implémentation:

        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
Fernando Ribeiro