Вопрос

У меня возникла небольшая проблема (больше похожая на раздражение) с моими классами доступа к данным, привязывающими свойства.Проблема в том, что сопоставление завершается с ошибкой, когда в reader не существует столбца для соответствующего свойства в классе.

Код

Вот класс картографа:

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

Класс преобразователя:

/// <summary>
/// Converter class to convert returned Sql Records to strongly typed classes
/// </summary>
/// <typeparam name="T">Type of the object we'll convert too</typeparam>
internal class Converter<T> where T : new()
{
    // Concurrent Dictionay objects
    private static ConcurrentDictionary<Type, object> _convertActionMap = new ConcurrentDictionary<Type, object>();
    // Delegate action declaration
    private Action<IDataReader, T> _convertAction;

    // Build our mapping based on the properties in the class/type we've passed in to the class
    private static Action<IDataReader, T> GetMapFunc()
    {
        var exps = new List<Expression>();
        var paramExp = Expression.Parameter(typeof(IDataReader), "o7thDR");
        var targetExp = Expression.Parameter(typeof(T), "o7thTarget");
        var getPropInfo = typeof(IDataRecord).GetProperty("Item", new[] { typeof(string) });
        var _props = typeof(T).GetProperties();
        foreach (var property in _props)
        {

            var getPropExp = Expression.MakeIndex(paramExp, getPropInfo, new[] { Expression.Constant(property.Name, typeof(string)) });
            var castExp = Expression.TypeAs(getPropExp, property.PropertyType);
            var bindExp = Expression.Assign(Expression.Property(targetExp, property), castExp);
            exps.Add(bindExp);

        }
        // return our compiled mapping, this will ensure it is cached to use through our record looping
        return Expression.Lambda<Action<IDataReader, T>>(Expression.Block(exps), new[] { paramExp, targetExp }).Compile();
    }

    internal Converter()
    {
        // Fire off our mapping functionality
        _convertAction = (Action<IDataReader, T>)_convertActionMap.GetOrAdd(typeof(T), (t) => GetMapFunc());
    }

    internal T CreateItemFromRow(IDataReader dataReader)
    {
        T result = new T();
        _convertAction(dataReader, result);
        return result;
    }
}

Исключение

System.IndexOutOfRangeException {"Mileage"}

Отслеживание стека

at System.Data.ProviderBase.FieldNameLookup.GetOrdinal(String fieldName)
at System.Data.SqlClient.SqlDataReader.GetOrdinal(String name)
at System.Data.SqlClient.SqlDataReader.get_Item(String name)
at lambda_method(Closure , IDataReader , Typing )
at o7th.Class.Library.Data.Converter`1.CreateItemFromRow(IDataReader dataReader) in d:\Backup Folder\Development\o7th Web Design\o7th.Class.Library.C-Sharp\o7th.Class.Library\Data Access Object\Converter.cs:line 50
at o7th.Class.Library.Data.Wrapper.Map[T](DbDataReader dr) in d:\Backup Folder\Development\o7th Web Design\o7th.Class.Library.C-Sharp\o7th.Class.Library\Data Access Object\Wrapper.cs:line 33

Вопрос

Как я могу это исправить, чтобы он не выходил из строя, когда у меня есть дополнительное свойство, которого у читателя может не быть в виде столбца, и наоборот?Конечно, самым быстрым способом было бы просто добавить пластырь NULL As Mileage однако для этого запроса в примере это не является решением проблемы :)


Вот Map<T> использование отражения:

// 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>();
        T item = new T();
        PropertyInfo[] properties = (item.GetType()).GetProperties();
        while (dr.Read()) {
            int fc = dr.FieldCount;
            for (int j = 0; j < fc; ++j) {
                var pn = properties[j].Name;
                var gn = dr.GetName(j);
                if (gn == pn) {
                    properties[j].SetValue(item, dr[j], null);
                }
            }
            list.Add(item);
        }
        // return it
        return list;
    }
    catch (Exception ex)
    {
        // Catch an exception if any, an write it out to our logging mechanism, in addition to adding it our returnable message property
        _Msg += "Wrapper.Map Exception: " + ex.Message;
        ErrorReporting.WriteEm.WriteItem(ex, "o7th.Class.Library.Data.Wrapper.Map", _Msg);
        // make sure this method returns a default List
        return default(List<T>);
    }
}

Примечание: Этот метод на 63% медленнее, чем использование деревьев выражений...

Это было полезно?

Решение

Как отмечалось в комментариях, проблема заключается в том, что в программе чтения не существует столбца для указанного свойства.Идея состоит в том, чтобы сначала выполнить цикл по именам столбцов reader и проверить, существует ли соответствующее свойство.Но как заранее получить список названий столбцов?

  1. Одна из идей состоит в том, чтобы использовать сами деревья выражений для создания списка имен столбцов из программы чтения и сверки его со свойствами класса.Что - то вроде этого

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

    было бы эквивалентно

    List<string> columnNames = new List<string>();
    for (int i = 0; i < reader.FieldCount; i++)
    {
        columnNames.Add(reader.GetName(i));
    }
    

    Можно продолжить с последним выражением, но здесь есть загвоздка, делающая любые дальнейшие усилия в этом направлении бесполезными.Приведенное выше дерево выражений будет извлекать имена столбцов каждый раз при вызове конечного делегата, который в вашем случае используется для каждого создания объекта, что противоречит духу вашего требования.

  2. Другой подход заключается в том, чтобы позволить классу converter заранее знать имена столбцов для данного типа с помощью атрибутов (смотрите пример) или путем поддержания статического словаря, подобного (Dictionary<Type, IEnumerable<string>>).Хотя это дает большую гибкость, оборотной стороной является то, что ваш запрос не всегда должен включать имена всех столбцов таблицы и любые reader[notInTheQueryButOnlyInTheTableColumn] это привело бы к исключению.

  3. Лучший подход, на мой взгляд, заключается в извлечении имен столбцов из объекта reader, но только один раз.Я бы переписал эту вещь следующим образом:

    private static List<string> columnNames;
    
    private static Action<IDataReader, T> GetMapFunc()
    {
        var exps = new List<Expression>();
    
        var paramExp = Expression.Parameter(typeof(IDataRecord), "o7thDR");
        var targetExp = Expression.Parameter(typeof(T), "o7thTarget");
    
        var getPropInfo = typeof(IDataRecord).GetProperty("Item", new[] { typeof(string) });
    
        foreach (var columnName in columnNames)
        {
            var property = typeof(T).GetProperty(columnName);
            if (property == null)
                continue;
    
            // use 'columnName' instead of 'property.Name' to speed up reader lookups
            //in case of certain readers.
            var columnNameExp = Expression.Constant(columnName);
            var getPropExp = Expression.MakeIndex(
                paramExp, getPropInfo, new[] { columnNameExp });
            var castExp = Expression.TypeAs(getPropExp, property.PropertyType);
            var bindExp = Expression.Assign(
                Expression.Property(targetExp, property), castExp);
            exps.Add(bindExp);
        }
    
        return Expression.Lambda<Action<IDataReader, T>>(
            Expression.Block(exps), paramExp, targetExp).Compile();
    }
    
    internal T CreateItemFromRow(IDataReader dataReader)
    {
        if (columnNames == null)
        {
            columnNames = Enumerable.Range(0, dataReader.FieldCount)
                                    .Select(x => dataReader.GetName(x))
                                    .ToList();
            _convertAction = (Action<IDataReader, T>)_convertActionMap.GetOrAdd(
                typeof(T), (t) => GetMapFunc());
        }
    
        T result = new T();
        _convertAction(dataReader, result);
        return result;
    }
    

    Теперь возникает вопрос, почему бы не передать средство чтения данных непосредственно конструктору?Так было бы лучше.

    private IDataReader dataReader;
    
    private Action<IDataReader, T> GetMapFunc()
    {
        var exps = new List<Expression>();
    
        var paramExp = Expression.Parameter(typeof(IDataRecord), "o7thDR");
        var targetExp = Expression.Parameter(typeof(T), "o7thTarget");
    
        var getPropInfo = typeof(IDataRecord).GetProperty("Item", new[] { typeof(string) });
    
        var columnNames = Enumerable.Range(0, dataReader.FieldCount)
                                    .Select(x => dataReader.GetName(x));
        foreach (var columnName in columnNames)
        {
            var property = typeof(T).GetProperty(columnName);
            if (property == null)
                continue;
    
            // use 'columnName' instead of 'property.Name' to speed up reader lookups
            //in case of certain readers.
            var columnNameExp = Expression.Constant(columnName);
            var getPropExp = Expression.MakeIndex(
                paramExp, getPropInfo, new[] { columnNameExp });
            var castExp = Expression.TypeAs(getPropExp, property.PropertyType);
            var bindExp = Expression.Assign(
                Expression.Property(targetExp, property), castExp);
            exps.Add(bindExp);
        }
    
        return Expression.Lambda<Action<IDataReader, T>>(
            Expression.Block(exps), paramExp, targetExp).Compile();
    }
    
    internal Converter(IDataReader dataReader)
    {
        this.dataReader = dataReader;
        _convertAction = (Action<IDataReader, T>)_convertActionMap.GetOrAdd(
            typeof(T), (t) => GetMapFunc());
    }
    
    internal T CreateItemFromRow()
    {
        T result = new T();
        _convertAction(dataReader, result);
        return result;
    }
    

    Назовите это так

    List<T> list = new List<T>();
    var converter = new Converter<T>(dr);
    while (dr.Read())
    {
        var obj = converter.CreateItemFromRow();
        list.Add(obj);
    }
    

Однако есть ряд улучшений, которые я могу предложить.

  1. Общий new T() ты звонишь по телефону CreateItemFromRow медленнее, он использует отражение за кулисами.Вы также можете делегировать эту часть деревьям выражений что должно быть быстрее

  2. Прямо сейчас GetProperty call не чувствителен к регистру, что означает, что имена ваших столбцов должны точно совпадать с именем свойства.Я бы сделал это без учета регистра, используя один из них Bindings.Flag.

  3. Я совсем не уверен, почему вы используете ConcurrentDictionary в качестве механизма кэширования здесь. Статическое поле в универсальном классе <T> будет уникальным для каждого T.Само универсальное поле может выступать в качестве кэша.Кроме того, почему Value часть ConcurrentDictionary типа object?

  4. Как я уже говорил ранее, не стоит сильно привязывать тип и имена столбцов (что вы делаете, кэшируя один конкретный Action делегировать на Тип).Даже для одного и того же типа ваши запросы могут отличаться при выборе разного набора столбцов.Лучше всего оставить это на усмотрение читателя данных.

  5. Использование Expression.Convert вместо того, чтобы Expression.TypeAs для преобразования типа значения из object.

  6. Также обратите внимание, что считыватель.GetOrdinal - это гораздо более быстрый способ выполнения поиска в программе чтения данных.

Я бы переписал все это заново следующим образом:

readonly Func<IDataReader, T> _converter;
readonly IDataReader dataReader;

private Func<IDataReader, T> GetMapFunc()
{
    var exps = new List<Expression>();

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

    var targetExp = Expression.Variable(typeof(T));
    exps.Add(Expression.Assign(targetExp, Expression.New(targetExp.Type)));

    //does int based lookup
    var indexerInfo = typeof(IDataRecord).GetProperty("Item", new[] { typeof(int) });

    var columnNames = Enumerable.Range(0, dataReader.FieldCount)
                                .Select(i => new { i, name = dataReader.GetName(i) });
    foreach (var column in columnNames)
    {
        var property = targetExp.Type.GetProperty(
            column.name,
            BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
        if (property == null)
            continue;

        var columnNameExp = Expression.Constant(column.i);
        var propertyExp = Expression.MakeIndex(
            paramExp, indexerInfo, new[] { columnNameExp });
        var convertExp = Expression.Convert(propertyExp, property.PropertyType);
        var bindExp = Expression.Assign(
            Expression.Property(targetExp, property), convertExp);
        exps.Add(bindExp);
    }

    exps.Add(targetExp);
    return Expression.Lambda<Func<IDataReader, T>>(
        Expression.Block(new[] { targetExp }, exps), paramExp).Compile();
}

internal Converter(IDataReader dataReader)
{
    this.dataReader = dataReader;
    _converter = GetMapFunc();
}

internal T CreateItemFromRow()
{
    return _converter(dataReader);
}
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top