DbDataReader générique à la liste <T> cartographie

c# dbdatareader expression-trees linq-expressions reflection

Question

J'ai un léger problème (plutôt une gêne) avec mes classes d'accès aux données de liaison de propriété. Le problème est que le mappage échoue lorsqu'il n'existe aucune colonne dans le lecteur pour la propriété correspondante dans la classe.

Code

Voici la classe de mappeur:

// Map our datareader object to a strongly typed list
private static IList<T> Map<T>(DbDataReader dr) where T : new()
{
    try
    {
        // initialize our returnable list
        List<T> list = new List<T>();
        // fire up the lamda mapping
        var converter = new Converter<T>();
        while (dr.Read())
        {
            // read in each row, and properly map it to our T object
            var obj = converter.CreateItemFromRow(dr);
            // add it to our list
            list.Add(obj);
        }
        // reutrn it
        return list;
    }
    catch (Exception ex)
    {    
        return default(List<T>);
    }
}

Classe de convertisseur:

// Map our datareader object to a strongly typed list
private static IList<T> Map<T>(DbDataReader dr) where T : new()
{
    try
    {
        // initialize our returnable list
        List<T> list = new List<T>();
        // fire up the lamda mapping
        var converter = new Converter<T>();
        while (dr.Read())
        {
            // read in each row, and properly map it to our T object
            var obj = converter.CreateItemFromRow(dr);
            // add it to our list
            list.Add(obj);
        }
        // reutrn it
        return list;
    }
    catch (Exception ex)
    {    
        return default(List<T>);
    }
}

Exception

// Map our datareader object to a strongly typed list
private static IList<T> Map<T>(DbDataReader dr) where T : new()
{
    try
    {
        // initialize our returnable list
        List<T> list = new List<T>();
        // fire up the lamda mapping
        var converter = new Converter<T>();
        while (dr.Read())
        {
            // read in each row, and properly map it to our T object
            var obj = converter.CreateItemFromRow(dr);
            // add it to our list
            list.Add(obj);
        }
        // reutrn it
        return list;
    }
    catch (Exception ex)
    {    
        return default(List<T>);
    }
}

Trace de la pile

// Map our datareader object to a strongly typed list
private static IList<T> Map<T>(DbDataReader dr) where T : new()
{
    try
    {
        // initialize our returnable list
        List<T> list = new List<T>();
        // fire up the lamda mapping
        var converter = new Converter<T>();
        while (dr.Read())
        {
            // read in each row, and properly map it to our T object
            var obj = converter.CreateItemFromRow(dr);
            // add it to our list
            list.Add(obj);
        }
        // reutrn it
        return list;
    }
    catch (Exception ex)
    {    
        return default(List<T>);
    }
}

Question

Comment puis-je résoudre ce problème, afin qu'il n'échoue pas lorsque j'ai une propriété supplémentaire que le lecteur peut ne pas avoir comme colonne et vice versa? Bien sûr, la solution rapide consisterait simplement à ajouter NULL As Mileage à cette requête, mais ce n’est pas une solution au problème :)


Voici la Map<T> utilisant la réflexion:

// Map our datareader object to a strongly typed list
private static IList<T> Map<T>(DbDataReader dr) where T : new()
{
    try
    {
        // initialize our returnable list
        List<T> list = new List<T>();
        // fire up the lamda mapping
        var converter = new Converter<T>();
        while (dr.Read())
        {
            // read in each row, and properly map it to our T object
            var obj = converter.CreateItemFromRow(dr);
            // add it to our list
            list.Add(obj);
        }
        // reutrn it
        return list;
    }
    catch (Exception ex)
    {    
        return default(List<T>);
    }
}

Remarque: cette méthode est 63% plus lente que l'utilisation des arbres d'expression ...

Réponse acceptée

Comme indiqué dans les commentaires, le problème est qu'il n'existe aucune colonne dans le lecteur pour la propriété spécifiée. L'idée est de commencer par lire les noms de colonne du lecteur et de vérifier si la propriété correspondante existe. Mais comment obtenir la liste des noms de colonnes à l’avance?

  1. Une idée consiste à utiliser les arbres d'expression eux-mêmes pour créer la liste des noms de colonnes à partir du lecteur et la comparer aux propriétés de la classe. Quelque chose comme ça

    var paramExp = Expression.Parameter(typeof(IDataRecord), "o7thDR");
    
    var loopIncrementVariableExp = Expression.Parameter(typeof(int), "i");
    var columnNamesExp = Expression.Parameter(typeof(List<string>), "columnNames");
    
    var columnCountExp = Expression.Property(paramExp, "FieldCount");
    var getColumnNameExp = Expression.Call(paramExp, "GetName", Type.EmptyTypes, 
        Expression.PostIncrementAssign(loopIncrementVariableExp));
    var addToListExp = Expression.Call(columnNamesExp, "Add", Type.EmptyTypes, 
        getColumnNameExp);
    var labelExp = Expression.Label(columnNamesExp.Type);
    
    var getColumnNamesExp = Expression.Block(
        new[] { loopIncrementVariableExp, columnNamesExp },
        Expression.Assign(columnNamesExp, Expression.New(columnNamesExp.Type)),
        Expression.Loop(
            Expression.IfThenElse(
                Expression.LessThan(loopIncrementVariableExp, columnCountExp),
                addToListExp,
                Expression.Break(labelExp, columnNamesExp)),
            labelExp));
    

    serait l'équivalent de

    var paramExp = Expression.Parameter(typeof(IDataRecord), "o7thDR");
    
    var loopIncrementVariableExp = Expression.Parameter(typeof(int), "i");
    var columnNamesExp = Expression.Parameter(typeof(List<string>), "columnNames");
    
    var columnCountExp = Expression.Property(paramExp, "FieldCount");
    var getColumnNameExp = Expression.Call(paramExp, "GetName", Type.EmptyTypes, 
        Expression.PostIncrementAssign(loopIncrementVariableExp));
    var addToListExp = Expression.Call(columnNamesExp, "Add", Type.EmptyTypes, 
        getColumnNameExp);
    var labelExp = Expression.Label(columnNamesExp.Type);
    
    var getColumnNamesExp = Expression.Block(
        new[] { loopIncrementVariableExp, columnNamesExp },
        Expression.Assign(columnNamesExp, Expression.New(columnNamesExp.Type)),
        Expression.Loop(
            Expression.IfThenElse(
                Expression.LessThan(loopIncrementVariableExp, columnCountExp),
                addToListExp,
                Expression.Break(labelExp, columnNamesExp)),
            labelExp));
    

    On peut continuer avec l'expression finale, mais il y a un problème ici qui rend inutile tout effort supplémentaire le long de cette ligne. L'arbre d'expression ci-dessus récupérera les noms de colonne chaque fois que le dernier délégué sera appelé, ce qui dans votre cas correspond à chaque création d'objet, ce qui est contraire à l'esprit de votre exigence.

  2. Une autre approche consiste à laisser à la classe du convertisseur une connaissance prédéfinie des noms de colonne d’un type donné, au moyen d’attributs ( voir un exemple ) ou en conservant un dictionnaire statique du type ( Dictionary<Type, IEnumerable<string>> ). Bien que cela donne plus de flexibilité, le problème est que votre requête n'a pas toujours besoin d'inclure tous les noms de colonne d'une table, et tout reader[notInTheQueryButOnlyInTheTableColumn] entraînerait une exception.

  3. La meilleure approche que je vois est d'extraire les noms de colonnes de l'objet lecteur, mais une seule fois. Je réécrirais la chose comme:

    var paramExp = Expression.Parameter(typeof(IDataRecord), "o7thDR");
    
    var loopIncrementVariableExp = Expression.Parameter(typeof(int), "i");
    var columnNamesExp = Expression.Parameter(typeof(List<string>), "columnNames");
    
    var columnCountExp = Expression.Property(paramExp, "FieldCount");
    var getColumnNameExp = Expression.Call(paramExp, "GetName", Type.EmptyTypes, 
        Expression.PostIncrementAssign(loopIncrementVariableExp));
    var addToListExp = Expression.Call(columnNamesExp, "Add", Type.EmptyTypes, 
        getColumnNameExp);
    var labelExp = Expression.Label(columnNamesExp.Type);
    
    var getColumnNamesExp = Expression.Block(
        new[] { loopIncrementVariableExp, columnNamesExp },
        Expression.Assign(columnNamesExp, Expression.New(columnNamesExp.Type)),
        Expression.Loop(
            Expression.IfThenElse(
                Expression.LessThan(loopIncrementVariableExp, columnCountExp),
                addToListExp,
                Expression.Break(labelExp, columnNamesExp)),
            labelExp));
    

    Maintenant, cela pose la question pourquoi ne pas passer le lecteur de données directement au constructeur? Ça serait mieux.

    var paramExp = Expression.Parameter(typeof(IDataRecord), "o7thDR");
    
    var loopIncrementVariableExp = Expression.Parameter(typeof(int), "i");
    var columnNamesExp = Expression.Parameter(typeof(List<string>), "columnNames");
    
    var columnCountExp = Expression.Property(paramExp, "FieldCount");
    var getColumnNameExp = Expression.Call(paramExp, "GetName", Type.EmptyTypes, 
        Expression.PostIncrementAssign(loopIncrementVariableExp));
    var addToListExp = Expression.Call(columnNamesExp, "Add", Type.EmptyTypes, 
        getColumnNameExp);
    var labelExp = Expression.Label(columnNamesExp.Type);
    
    var getColumnNamesExp = Expression.Block(
        new[] { loopIncrementVariableExp, columnNamesExp },
        Expression.Assign(columnNamesExp, Expression.New(columnNamesExp.Type)),
        Expression.Loop(
            Expression.IfThenElse(
                Expression.LessThan(loopIncrementVariableExp, columnCountExp),
                addToListExp,
                Expression.Break(labelExp, columnNamesExp)),
            labelExp));
    

    Appelez ça comme

    var paramExp = Expression.Parameter(typeof(IDataRecord), "o7thDR");
    
    var loopIncrementVariableExp = Expression.Parameter(typeof(int), "i");
    var columnNamesExp = Expression.Parameter(typeof(List<string>), "columnNames");
    
    var columnCountExp = Expression.Property(paramExp, "FieldCount");
    var getColumnNameExp = Expression.Call(paramExp, "GetName", Type.EmptyTypes, 
        Expression.PostIncrementAssign(loopIncrementVariableExp));
    var addToListExp = Expression.Call(columnNamesExp, "Add", Type.EmptyTypes, 
        getColumnNameExp);
    var labelExp = Expression.Label(columnNamesExp.Type);
    
    var getColumnNamesExp = Expression.Block(
        new[] { loopIncrementVariableExp, columnNamesExp },
        Expression.Assign(columnNamesExp, Expression.New(columnNamesExp.Type)),
        Expression.Loop(
            Expression.IfThenElse(
                Expression.LessThan(loopIncrementVariableExp, columnCountExp),
                addToListExp,
                Expression.Break(labelExp, columnNamesExp)),
            labelExp));
    

Je peux toutefois suggérer un certain nombre d'améliorations.

  1. Le new T() générique que vous appelez dans CreateItemFromRow est plus lent, il utilise la réflexion en arrière-plan . Vous pouvez également déléguer cette partie à des arbres d'expression, ce qui devrait être plus rapide.

  2. À l'heure GetProperty appel GetProperty n'est pas sensible à la casse, ce qui signifie que les noms de colonne doivent correspondre exactement au nom de la propriété. Je voudrais le rendre insensible à la casse en utilisant un de ces Bindings.Flag .

  3. Je ne sais pas du tout pourquoi vous utilisez un ConcurrentDictionary comme mécanisme de mise en cache ici. Un champ statique dans une classe générique <T> sera unique pour chaque T Le champ générique lui-même peut servir de cache. Aussi, pourquoi la partie Value de ConcurrentDictionary du type object ?

  4. Comme je l'ai dit plus tôt, il n'est pas préférable de lier fortement un type et les noms de colonnes (ce que vous faites en mettant en cache un délégué Action particulier par type ). Même pour le même type, vos requêtes peuvent être différentes en sélectionnant un ensemble de colonnes différent. Il est préférable de laisser le lecteur de données décider.

  5. Utilisez Expression.Convert au lieu de Expression.TypeAs pour la conversion de type de valeur à partir d' object .

  6. Notez également que reader.GetOrdinal est un moyen beaucoup plus rapide d’effectuer des recherches dans le lecteur de données.

Je réécrirais le tout comme:

var paramExp = Expression.Parameter(typeof(IDataRecord), "o7thDR");

var loopIncrementVariableExp = Expression.Parameter(typeof(int), "i");
var columnNamesExp = Expression.Parameter(typeof(List<string>), "columnNames");

var columnCountExp = Expression.Property(paramExp, "FieldCount");
var getColumnNameExp = Expression.Call(paramExp, "GetName", Type.EmptyTypes, 
    Expression.PostIncrementAssign(loopIncrementVariableExp));
var addToListExp = Expression.Call(columnNamesExp, "Add", Type.EmptyTypes, 
    getColumnNameExp);
var labelExp = Expression.Label(columnNamesExp.Type);

var getColumnNamesExp = Expression.Block(
    new[] { loopIncrementVariableExp, columnNamesExp },
    Expression.Assign(columnNamesExp, Expression.New(columnNamesExp.Type)),
    Expression.Loop(
        Expression.IfThenElse(
            Expression.LessThan(loopIncrementVariableExp, columnCountExp),
            addToListExp,
            Expression.Break(labelExp, columnNamesExp)),
        labelExp));



Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Est-ce KB légal? Oui, apprenez pourquoi
Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Est-ce KB légal? Oui, apprenez pourquoi