Самый эффективный способ сопоставить IDataReader с объектом C#, также предназначенным для глубокого копирования?

У нас есть сценарий, в котором мы используем пакет данных Enterprise Library для целей наших потребностей в доступе к данным. Мы внедрили универсальный считыватель данных для гидратации наших данных в бизнес-объекты. Однако мы ищем более эффективный способ сделать это, который также будет обслуживать глубокое копирование дочерних коллекций внутри объектов без использования отражения, поскольку мы имеем дело с большими объемами данных. Generic Data Reader также плохо работает с большими объемами данных. Я знаю, если бы мы использовали EF, это могло бы решить эту проблему, но мы не на той стадии, на которой мы можем заменить весь наш уровень доступа к данным. Есть ли альтернатива или отраслевой стандарт для достижения этого?

2 ответа

Решение

Почему бы вам не написать код картографов? Это самый быстрый способ сделать это, и вы создаете маппер только один раз за время существования проекта, верно? В проектах, живущих пять-десять лет, количество времени, которое требуется, ничтожно мало по сравнению с тем, что вы получаете (легко исправлять ошибки, не нужно интерпретировать ошибки OR/M или их "простоту").

public interface IEntityMapper
{
   object Map(IDataRecord record);
}

public void UserMapper  : IEntityMapper
{
    public void Map(IDataRecord record)
    {
        var user = new User();
        user.FirstName = record["firstName"]
    }
}

Если вы получили очень большое количество записей, вам нужно использовать number вместо этого индексатор, чтобы предотвратить поиск имен:

public void UserMapper  : IEntityMapper
{
    public void Map(IDataRecord record)
    {
        var user = new User();

        //requires that you specify columns in your SELECT query
        //to not break the mapper in future versions.
        user.FirstName = record[0]
    }
}

В вашем репозитории вы можете сделать так:

public class UserRepository
{
    private readonly IDbConnection _connection;
    private UserMapper _mapper = new UserMapper();

    public IReadOnlyList<User> GetAllUsers()
    {
        using (var cmd = _connection.CreateCommand())
        {
            cmd.CommandText = "SELECT Id, UserName FROM Users";
            using (var reader = cmd.ExecuteReader())
            {
                var users = new List<User>();
                while (reader.Read())
                {
                   var user = _mapper.Map(reader);
                   users.Add(user);
                }
                return users;
            }
        }
    }
}

Вы даже можете переместить все в метод расширения:

public static class CommandExtensions
{
    public static IReadOnlyCollection<T> ToList<T>(this IDbCommand cmd, IDataMapper mapper)
    {
        using (var reader = cmd.ExecuteReader())
        {
            var items = new List<T>();
            while (reader.Read())
            {
               var item= _mapper.Map(reader);
               items.Add(item);
            }
            return item;
        }
    }
}

что дает вам еще более легкие репозитории:

public class UserRepository
{
    private readonly IDbConnection _connection;
    private UserMapper _mapper = new UserMapper();

    public IReadOnlyList<User> GetAllUsers()
    {
        using (var cmd = _connection.CreateCommand())
        {
            cmd.CommandText = "SELECT Id, UserName FROM Users";
            return cmd.ToList<User>(_mapper);
        }
    }
}

Глубокая копия

Что касается глубоких копий, я старался избегать работы со сложными сущностями (если дочерние сущности на самом деле не являются объектами-значениями). Лучше ссылаться на другие объекты, используя идентификаторы, так как в противном случае трудно сохранить ответственность за объект в одном месте в вашем коде.

Когда дело доходит до отображения, я бы, вероятно, ввел соглашение, согласно которому подобъекты отображаются с использованием альтернативного метода в интерфейсе отображения:

public interface IEntityMapper
{
   object Map(IDataRecord record);
   object Map(IDataRecord record, string prefix);
}

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

public void UserMapper  : IEntityMapper
{
    private AddressMapper _childMapper = new AddressMapper();

    public void Map(IDataRecord record)
    {
        var user = new User();
        user.FirstName = record["FirstName"]
        user.Address = _childMapper.Map(record, "Address_");
    }
}

.. который сделал бы ваш метод хранилища похожим на это:

public class UserRepository
{
    private readonly IDbConnection _connection;
    private UserMapper _mapper = new UserMapper();

    public IReadOnlyList<User> GetAllUsers()
    {
        using (var cmd = _connection.CreateCommand())
        {
            cmd.CommandText = @"SELECT Id, UserName, Address.City as Address_City 
                                FROM Users 
                                JOIN Address ON (Address.Id = Users.AddressId)";
            return cmd.ToList<User>(_mapper);
        }
    }
}

Я написал об ADO.NET и (моих) лучших практиках: http://blog.gauffin.org/2013/01/04/ado-net-the-right-way/

Используя , вы можете динамически создавать динамический код, эффективность которого близка к жестко запрограммированной.

Давайте вручную создадим простое ядро ​​ADO:

      // Code by Flithor(Mr. Squirrel.Downy)
// License: MIT
// == Warning ==
// Does not work in every possible situation
// Use at your own risk

/// <summary>
/// Getter <see cref="MethodInfo"/> of each common type of <see cref="IDataRecord"/>
/// </summary>
private static readonly Dictionary<Type, MethodInfo> IDataRecordGetterMethods = typeof(IDataRecord).GetMethods()
    .Where(m => m.GetParameters().Length == 1 && m.Name == "Get" + m.ReturnType.Name)
    .ToDictionary(m => m.ReturnType);
/// <summary>
/// The <see cref="MethodInfo"/> of<see cref="IDataRecord.IsDBNull"/> of <see cref="IDataRecord"/>
/// </summary>
private static readonly MethodInfo isDbNullMethod = typeof(IDataRecord).GetMethod(nameof(IDataRecord.IsDBNull));

/// <summary>
/// Convert <see cref="IDataReader "/> to an entity sequence
/// </summary>
public static IEnumerable<T> CastTo<T>(this IDataReader reader) where T : new()
{
    Func<IDataRecord, T> instanceBuilder = null;
    while (reader.Read())
    {
        if (instanceBuilder == null)
        {
            var sw = Stopwatch.StartNew();
            // get orderd fields from first line
            var fields = Enumerable.Range(0, reader.FieldCount).Select(reader.GetName).ToArray();
            instanceBuilder = BuildAssginer<T>(fields);
            // build lambda just spent about 10 ms
            Console.WriteLine($"Build lambda cost:{sw.Elapsed}");
        }

        yield return instanceBuilder(reader);
    }
}

/// <summary>
/// Build Entity builder lambda
/// </summary>
/// <param name="fields">Orderd fields of result datas</param>
private static Func<IDataRecord, TResult> BuildAssginer<TResult>(string[] fields) where TResult : new()
{
    var createdType = typeof(TResult);

    // new Tresult()
    var ctor = Expression.New(createdType);
    var properties = typeof(TResult).GetProperties().ToDictionary(pinfo => pinfo.Name.ToLower());

    //Map indexs of fields on PropertyInfo, for efficiency
    var indexToPropMap = fields.Select((field, index) => new { field, index })
        .GroupJoin(properties, o => o.field.ToLower(), prop => prop.Key.ToLower(),
            (o, props) => new { o.index, prop = props.FirstOrDefault().Value })
        .Where(o => o.prop != null)
        .OrderBy(o => o.index)
        .ToDictionary(o => o.index, o => o.prop);
    //define lambda input parameter of IDataRecord type
    var recordParamExp = Expression.Parameter(typeof(IDataRecord));
    //get property 
    var propAssginExps = indexToPropMap.Select(kv =>
    {
        // get field type and the underlying type of nullable type(maybe)
        var propType = kv.Value.PropertyType;
        var underType = Nullable.GetUnderlyingType(propType);
        // build get value expression for most most appropriate case
        var getValuesExp = underType != null
            ? Expression.Convert(
                Expression.Call(recordParamExp, IDataRecordGetterMethods[underType],
                    Expression.Constant(kv.Key)), propType)
            : (Expression)Expression.Call(recordParamExp, IDataRecordGetterMethods[propType],
                Expression.Constant(kv.Key));
        // is not value type or is nullable
        var propCanNull = !propType.IsValueType || underType != null;
        var getFieldValueExp = propCanNull
            // may null field check is dbnull or not
            ? Expression.Condition(
                Expression.Call(recordParamExp, isDbNullMethod, Expression.Constant(kv.Key)),
                Expression.Default(propType),
                getValuesExp)
            // return not nullable value directly
            : getValuesExp;
        // set field
        return Expression.Bind(kv.Value, getFieldValueExp);
    });
    // entity initializer
    // sample:
    // new TResult
    // {
    //     No = record.GetInt32(0),
    //     Name = record.GetString(1),
    //     Num = record.IsNull(2) ? default(Int64) : record.GetInt64(2)
    // }
    var memberInit = Expression.MemberInit(ctor, propAssginExps);
    return Expression.Lambda<Func<IDataRecord, TResult>>(memberInit, recordParamExp).Compile();
}

Зная, как использовать, вы можете написать глубокую копию самостоятельно.

Другие вопросы по тегам