Вручную сопоставьте имена столбцов со свойствами класса

Я новичок в Dapper Micro ORM. Пока я могу использовать его для простых вещей, связанных с ORM, но я не могу сопоставить имена столбцов базы данных со свойствами класса. Например:

У меня есть таблица базы данных следующим образом:

Table Name: Person
person_id  int
first_name varchar(50)
last_name  varchar(50)

и у меня есть класс под названием Person

public class Person 
{
    public int PersonId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

Обратите внимание, что имена моих столбцов в таблице отличаются от имени свойства класса, в который я пытаюсь отобразить данные, полученные из результата запроса.

var sql = @"select top 1 PersonId,FirstName,LastName from Person";
using (var conn = ConnectionFactory.GetConnection())
{
    var person = conn.Query<Person>(sql).ToList();
    return person;
}

Приведенный выше код не будет работать, так как имена столбцов не будут совпадать со свойствами объекта (Person). В этом сценарии есть что-нибудь, что я могу сделать в Dapper, чтобы вручную отобразить (например, person_id => PersonId) имена столбцов со свойствами объекта?

Любая подсказка или помощь будут высоко оценены.

18 ответов

Решение

Это отлично работает:

var sql = @"select top 1 person_id PersonId, first_name FirstName, last_name LastName from Person";
using (var conn = ConnectionFactory.GetConnection())
{
    var person = conn.Query<Person>(sql).ToList();
    return person;
}

У Dapper нет средства, позволяющего указывать атрибут столбца, я не против добавления его поддержки, если мы не используем зависимость.

Dapper теперь поддерживает настраиваемый столбец для сопоставления свойств. Это происходит через интерфейс ITypeMap. Dapper предоставляет класс CustomPropertyTypeMap, который может выполнять большую часть этой работы. Например:

Dapper.SqlMapper.SetTypeMap(
    typeof(TModel),
    new CustomPropertyTypeMap(
        typeof(TModel),
        (type, columnName) =>
            type.GetProperties().FirstOrDefault(prop =>
                prop.GetCustomAttributes(false)
                    .OfType<ColumnAttribute>()
                    .Any(attr => attr.Name == columnName))));

И модель:

public class TModel {
    [Column(Name="my_property")]
    public int MyProperty { get; set; }
}

Важно отметить, что реализация CustomPropertyTypeMap требует, чтобы атрибут существовал и совпадал с одним из имен столбцов, иначе свойство не будет отображено. Класс DefaultTypeMap предоставляет стандартную функциональность и может быть использован для изменения этого поведения:

public class FallbackTypeMapper : SqlMapper.ITypeMap
{
    private readonly IEnumerable<SqlMapper.ITypeMap> _mappers;

    public FallbackTypeMapper(IEnumerable<SqlMapper.ITypeMap> mappers)
    {
        _mappers = mappers;
    }

    public SqlMapper.IMemberMap GetMember(string columnName)
    {
        foreach (var mapper in _mappers)
        {
            try
            {
                var result = mapper.GetMember(columnName);
                if (result != null)
                {
                    return result;
                }
            }
            catch (NotImplementedException nix)
            {
            // the CustomPropertyTypeMap only supports a no-args
            // constructor and throws a not implemented exception.
            // to work around that, catch and ignore.
            }
        }
        return null;
    }
    // implement other interface methods similarly

    // required sometime after version 1.13 of dapper
    public ConstructorInfo FindExplicitConstructor()
    {
        return _mappers
            .Select(mapper => mapper.FindExplicitConstructor())
            .FirstOrDefault(result => result != null);
    }
}

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

public class ColumnAttributeTypeMapper<T> : FallbackTypeMapper
{
    public ColumnAttributeTypeMapper()
        : base(new SqlMapper.ITypeMap[]
            {
                new CustomPropertyTypeMap(
                   typeof(T),
                   (type, columnName) =>
                       type.GetProperties().FirstOrDefault(prop =>
                           prop.GetCustomAttributes(false)
                               .OfType<ColumnAttribute>()
                               .Any(attr => attr.Name == columnName)
                           )
                   ),
                new DefaultTypeMap(typeof(T))
            })
    {
    }
}

Это означает, что теперь мы можем легко поддерживать типы, которым требуется карта, используя атрибуты:

Dapper.SqlMapper.SetTypeMap(
    typeof(MyModel),
    new ColumnAttributeTypeMapper<MyModel>());

Вот Gist для полного исходного кода.

В течение некоторого времени должно работать следующее:

Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;

Я делаю следующее, используя динамические и LINQ:

    var sql = @"select top 1 person_id, first_name, last_name from Person";
    using (var conn = ConnectionFactory.GetConnection())
    {
        List<Person> person = conn.Query<dynamic>(sql)
                                  .Select(item => new Person()
                                  {
                                      PersonId = item.person_id,
                                      FirstName = item.first_name,
                                      LastName = item.last_name
                                  }
                                  .ToList();

        return person;
    }

Вот простое решение, которое не требует атрибутов, позволяющих вам исключить код инфраструктуры из ваших POCO.

Это класс для работы с отображениями. Словарь будет работать, если вы отобразите все столбцы, но этот класс позволяет вам указать только различия. Кроме того, он включает в себя обратные карты, поэтому вы можете получить поле из столбца и столбец из поля, что может быть полезно при выполнении таких задач, как генерация операторов SQL.

public class ColumnMap
{
    private readonly Dictionary<string, string> forward = new Dictionary<string, string>();
    private readonly Dictionary<string, string> reverse = new Dictionary<string, string>();

    public void Add(string t1, string t2)
    {
        forward.Add(t1, t2);
        reverse.Add(t2, t1);
    }

    public string this[string index]
    {
        get
        {
            // Check for a custom column map.
            if (forward.ContainsKey(index))
                return forward[index];
            if (reverse.ContainsKey(index))
                return reverse[index];

            // If no custom mapping exists, return the value passed in.
            return index;
        }
    }
}

Настройте объект ColumnMap и скажите Dapper использовать сопоставление.

var columnMap = new ColumnMap();
columnMap.Add("Field1", "Column1");
columnMap.Add("Field2", "Column2");
columnMap.Add("Field3", "Column3");

SqlMapper.SetTypeMap(typeof (MyClass), new CustomPropertyTypeMap(typeof (MyClass), (type, columnName) => type.GetProperty(columnMap[columnName])));

Взято из Dapper Tests, которое в настоящее время находится на Dapper 1.42.

// custom mapping
var map = new CustomPropertyTypeMap(typeof(TypeWithMapping), 
                                    (type, columnName) => type.GetProperties().FirstOrDefault(prop => GetDescriptionFromAttribute(prop) == columnName));
Dapper.SqlMapper.SetTypeMap(typeof(TypeWithMapping), map);

Вспомогательный класс для получения имени из атрибута Description (лично я использовал Column, как пример @kalebs)

static string GetDescriptionFromAttribute(MemberInfo member)
{
   if (member == null) return null;

   var attrib = (DescriptionAttribute)Attribute.GetCustomAttribute(member, typeof(DescriptionAttribute), false);
   return attrib == null ? null : attrib.Description;
}

Учебный класс

public class TypeWithMapping
{
   [Description("B")]
   public string A { get; set; }

   [Description("A")]
   public string B { get; set; }
}

Простой способ добиться этого - просто использовать псевдонимы столбцов в вашем запросе. Если ваш столбец базы данных PERSON_ID и свойство вашего объекта ID ты можешь просто сделать select PERSON_ID as Id ... в вашем запросе и Dapper подберет его, как и ожидалось.

Прежде чем открыть соединение с вашей базой данных, выполните этот фрагмент кода для каждого из ваших классов Poco:

// Section
SqlMapper.SetTypeMap(typeof(Section), new CustomPropertyTypeMap(
    typeof(Section), (type, columnName) => type.GetProperties().FirstOrDefault(prop =>
    prop.GetCustomAttributes(false).OfType<ColumnAttribute>().Any(attr => attr.Name == columnName))));

Затем добавьте аннотации данных в ваши классы Poco следующим образом:

public class Section
{
    [Column("db_column_name1")] // Side note: if you create aliases, then they would match this.
    public int Id { get; set; }
    [Column("db_column_name2")]
    public string Title { get; set; }
}

После этого у вас все готово. Просто сделайте запрос, что-то вроде:

using (var sqlConnection = new SqlConnection("your_connection_string"))
{
    var sqlStatement = "SELECT " +
                "db_column_name1, " +
                "db_column_name2 " +
                "FROM your_table";

    return sqlConnection.Query<Section>(sqlStatement).AsList();
}

Смешивание с картографией - это пограничный переход в реальную землю ORM. Вместо того, чтобы бороться с этим и поддерживать Dapper в его истинно простой (быстрой) форме, просто слегка измените ваш SQL следующим образом:

var sql = @"select top 1 person_id as PersonId,FirstName,LastName from Person";

Если вы используете.NET 4.5.1 или выше, извлекайте Dapper.FluentColumnMapping для отображения стиля LINQ. Это позволяет полностью отделить отображение БД от вашей модели (нет необходимости в аннотациях)

Это свиное отступление от других ответов. Это просто мысль, которая у меня была для управления строками запроса.

Person.cs

public class Person 
{
    public int PersonId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public static string Select() 
    {
        return $"select top 1 person_id {nameof(PersonId)}, first_name {nameof(FirstName)}, last_name {nameof(LastName)}from Person";
    }
}

Метод API

using (var conn = ConnectionFactory.GetConnection())
{
    var person = conn.Query<Person>(Person.Select()).ToList();
    return person;
}

Простое решение проблемы, которую пытается решить Kaleb, - это просто принять имя свойства, если атрибут столбца не существует:

Dapper.SqlMapper.SetTypeMap(
    typeof(T),
    new Dapper.CustomPropertyTypeMap(
        typeof(T),
        (type, columnName) =>
            type.GetProperties().FirstOrDefault(prop =>
                prop.GetCustomAttributes(false)
                    .OfType<ColumnAttribute>()
                    .Any(attr => attr.Name == columnName) || prop.Name == columnName)));

Более простой способ (такой же, как ответ @Matt M, но исправлен и добавлен откат к карте по умолчанию)

      // override TypeMapProvider to return custom map for every requested type
Dapper.SqlMapper.TypeMapProvider = type =>
   {
       // create fallback default type map
       var fallback = new DefaultTypeMap(type);
       return new CustomPropertyTypeMap(type, (t, column) =>
       {
           var property = t.GetProperties().FirstOrDefault(prop =>
               prop.GetCustomAttributes(typeof(ColumnAttribute))
                   .Cast<ColumnAttribute>()
                   .Any(attr => attr.Name == column));

           // if no property matched - fall back to default type map
           if (property == null)
           {
               property = fallback.GetMember(column)?.Property;
           }

           return property;
       });
   };

Обратите внимание, что сопоставление объектов Dapper не чувствительно к регистру, поэтому вы можете называть свои свойства следующим образом:

public class Person 
{
    public int Person_Id { get; set; }
    public string First_Name { get; set; }
    public string Last_Name { get; set; }
}

Или оставьте класс Person и используйте PersonMap:

  public class PersonMap 
        {
            public int Person_Id { get; set; }
            public string First_Name { get; set; }
            public string Last_Name { get; set; }
            public Person Map(){
              return new Person{
                PersonId = Person_Id,
                FirstName = First_Name,
                LastName = Last_Name
               }               
            }
        }

А затем в результате запроса:

var person = conn.Query<PersonMap>(sql).Select(x=>x.Map()).ToList();

Для всех вас, кто использует Dapper 1.12, вот что вам нужно сделать, чтобы сделать это:

  • Добавьте новый класс атрибута столбца:

      [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property]
    
      public class ColumnAttribute : Attribute
      {
    
        public string Name { get; set; }
    
        public ColumnAttribute(string name)
        {
          this.Name = name;
        }
      }
    

  • Поиск по этой строке:

    map = new DefaultTypeMap(type);
    

    и закомментируйте это.

  • Напишите это вместо:

            map = new CustomPropertyTypeMap(type, (t, columnName) =>
            {
              PropertyInfo pi = t.GetProperties().FirstOrDefault(prop =>
                                prop.GetCustomAttributes(false)
                                    .OfType<ColumnAttribute>()
                                    .Any(attr => attr.Name == columnName));
    
              return pi != null ? pi : t.GetProperties().FirstOrDefault(prop => prop.Name == columnName);
            });
    

  • Решение Калеба Педерсона сработало для меня. Я обновил ColumnAttributeTypeMapper, чтобы разрешить настраиваемый атрибут (требовалось два разных сопоставления для одного и того же объекта домена), и обновил свойства, чтобы разрешить частные установщики в тех случаях, когда необходимо получить поле и типы различаются.

    public class ColumnAttributeTypeMapper<T,A> : FallbackTypeMapper where A : ColumnAttribute
    {
        public ColumnAttributeTypeMapper()
            : base(new SqlMapper.ITypeMap[]
                {
                    new CustomPropertyTypeMap(
                       typeof(T),
                       (type, columnName) =>
                           type.GetProperties( BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(prop =>
                               prop.GetCustomAttributes(true)
                                   .OfType<A>()
                                   .Any(attr => attr.Name == columnName)
                               )
                       ),
                    new DefaultTypeMap(typeof(T))
                })
        {
            //
        }
    }
    

    Я знаю, что это относительно старая тема, но я подумал, что брошу туда то, что сделал.

    Я хотел, чтобы отображение атрибутов работало глобально. Либо вы соответствуете имени свойства (по умолчанию), либо вы соответствуете атрибуту столбца в свойстве класса. Я также не хотел устанавливать это для каждого класса, который я отображал. Таким образом, я создал класс DapperStart, который я вызываю при запуске приложения:

    public static class DapperStart
    {
        public static void Bootstrap()
        {
            Dapper.SqlMapper.TypeMapProvider = type =>
            {
                return new CustomPropertyTypeMap(typeof(CreateChatRequestResponse),
                    (t, columnName) => t.GetProperties().FirstOrDefault(prop =>
                        {
                            return prop.Name == columnName || prop.GetCustomAttributes(false).OfType<ColumnAttribute>()
                                       .Any(attr => attr.Name == columnName);
                        }
                    ));
            };
        }
    }
    

    Довольно просто Не уверен, с какими проблемами я столкнусь еще, поскольку я только что написал это, но это работает.

    Я бы предложил решение, похожее на решение @liorafar, но основанное на словарях, а не на динамике:

          using var conn = ConnectionFactory.GetConnection();
    var person = conn.Query(sql)
        .Cast<IDictionary<string, object>>()
        .Select(record =>
            new Person
            {
                PersonId = (int)record["person_id"],
                FirstName = (string)record["first_name"],
                LastName = (string)record["last_name"],
            })
        .ToList();
    

    На мой взгляд, этот вариант более удобен для рефакторинга: например, вы можете объявлять имена столбцов как константы или читать их из конфигурации. Кроме того, в отличие от решения с динамикой, оно позволяет извлечь метод преобразования словаря в экземпляр модели (экземплярtype) для разделения метода, что особенно полезно для моделей со многими полями.

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