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


173

Я новичок в Dapper микро ОРМ. Пока я могу использовать его для простых вещей, связанных с 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) имена столбцов со свойствами объекта?


Ответы:


80

Это прекрасно работает:

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 нет средства, позволяющего указывать атрибут столбца , я не против добавления его поддержки, при условии, что мы не используем зависимость.


@Sam Saffron, я могу указать псевдоним таблицы. У меня есть класс с именем Country, но в базе данных таблица имеет очень запутанное имя из-за соглашений об именах.
TheVillageIdiot

64
Атрибут столбца был бы удобен для отображения результатов хранимых процедур.
Ронни Оверби

2
Атрибуты столбцов также будут полезны для облегчения тесной физической и / или семантической связи между вашим доменом и деталями реализации инструмента, которые вы используете для материализации ваших сущностей. Поэтому не добавляйте поддержку этого !!!! :)
Дерек Грир

Я не понимаю, почему столбца нет, когда таблица указана. Как этот пример будет работать со вставками, обновлениями и SP? Я хотел бы видеть columnattribe, его очень просто и очень легко перенести жизнь из других решений, которые реализуют нечто подобное, например, теперь уже не существующий linq-sql.
Вман

197

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

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.SqlMapper.SetTypeMap (typeof (MyModel)", new ColumnAttributeTypeMapper <MyModel> ()); " stackoverflow.com/questions/14814972/…
Рохан Бюхнер

Вы захотите позвонить один раз, прежде чем делать какие-либо запросы. Например, вы можете сделать это в статическом конструкторе, поскольку его нужно вызывать только один раз.
Калеб Педерсон

7
Рекомендую сделать это официальным ответом - эта функция Dapper чрезвычайно полезна.
killthrush

3
Картографическое решение, опубликованное @Oliver ( stackoverflow.com/a/34856158/364568 ), работает и требует меньше кода.
Рига,

4
Мне нравится, как слово «легко» разбрасывается так легко: P
Джонатан Б.

80

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

Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;

6
Хотя это не совсем ответ на вопрос « Вручную сопоставлять имена столбцов со свойствами класса», для меня это гораздо лучше, чем вручную сопоставлять (к сожалению, в PostgreSQL лучше использовать подчеркивания в именах столбцов). Пожалуйста, не удаляйте параметр MatchNamesWithUnderscores в следующих версиях! Спасибо!!!
victorvartan

5
@victorvartan нет планов по удалению этой MatchNamesWithUnderscoresопции. В лучшем случае, если бы мы провели рефакторинг API конфигурации, я бы оставил MatchNamesWithUnderscoresчлен на месте (который все еще работает, в идеале) и добавил бы [Obsolete]маркер, чтобы указать людям на новый API.
Марк Гравелл

4
@MarcGravell слова "Некоторое время" в начале вашего ответа заставили меня беспокоиться, что вы можете удалить его в будущей версии, спасибо за разъяснения! И большое спасибо за Dapper, замечательный микро ORM, который я только начал использовать для крошечного проекта вместе с Npgsql на ASP.NET Core!
victorvartan

2
Это легко лучший ответ. Я нашел груды и кучу работ вокруг, но в конце концов наткнулся на это. Легко лучший, но наименее разрекламированный ответ.
teaMonkeyFruit

29

Вот простое решение, которое не требует атрибутов, позволяющих вам исключить код инфраструктуры из ваших 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])));

Это хорошее решение, когда у вас в POCO есть несоответствие свойств тому, что ваша база данных возвращает, например, из хранимой процедуры.
раздавить

1
Мне нравится краткость, которую дает использование атрибута, но концептуально этот метод более чистый - он не связывает ваш POCO с деталями базы данных.
Бруно Брант

Если я правильно понимаю Dapper, у него нет определенного метода Insert (), только Execute () ... будет ли этот подход отображения работать для вставок? Или обновления? Спасибо
UuDdLrLrSs

29

Я делаю следующее, используя динамические и 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;
    }

12

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


12

Взято из 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; }
}

2
Для того , чтобы он работал даже для свойств , где нет описания не определенно, я изменил возвращение GetDescriptionFromAttributeк return (attrib?.Description ?? member.Name).ToLower();и добавил .ToLower()к columnNameв карте не должно быть чувствительны к регистру.
Сэм Уайт

11

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

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

8

Прежде чем открывать соединение с вашей базой данных, выполните этот фрагмент кода для каждого из ваших классов 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();
}

1
Все свойства должны иметь атрибут Column. Есть ли способ сопоставить со свойством в случае, если картограф недоступен?
sandeep.gosavi

5

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


5
Я являюсь автором Dapper.FluentColumnMapping. Отделение отображений от моделей было одной из основных целей проектирования. Я хотел изолировать основной доступ к данным (то есть интерфейсы репозитория, объекты моделей и т. Д.) От конкретных реализаций базы данных для четкого разделения проблем. Спасибо за упоминание, и я рад, что вы нашли это полезным! :-)
Александр

github.com/henkmollema/Dapper-FluentMap похож. Но вам больше не нужен сторонний пакет. Dapper добавил Dapper.SqlMapper. Смотрите мой ответ для более подробной информации, если вы заинтересованы.
Тадей

4

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

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

1

для всех вас, кто использует 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);
            });


  • Я не уверен, что понимаю - вы рекомендуете пользователям изменять Dapper, чтобы сделать возможным сопоставление атрибутов по столбцам? Если это так, то можно использовать код, который я разместил выше, без внесения изменений в Dapper.
    Калеб Педерсон

    1
    Но тогда вам придется вызывать функцию сопоставления для каждого из ваших типов моделей, не так ли? Я заинтересован в общем решении, чтобы все мои типы могли использовать атрибут, не вызывая отображение для каждого типа.
    Ури Абрамсон

    2
    Мне бы хотелось, чтобы DefaultTypeMap был реализован с использованием шаблона стратегии, который можно заменить по той причине, о которой упоминает @UriAbramson. См. Code.google.com/p/dapper-dot-net/issues/detail?id=140
    Ричард Коллетт,

    1

    Решение Калеба Педерсона сработало для меня. Я обновил 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))
                })
        {
            //
        }
    }

    1

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

    Я хотел, чтобы отображение атрибутов работало глобально. Либо вы соответствуете имени свойства (по умолчанию), либо вы соответствуете атрибуту столбца в свойстве класса. Я также не хотел устанавливать это для каждого класса, который я отображал. Таким образом, я создал класс 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);
                        }
                    ));
            };
        }
    }

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


    Как выглядит CreateChatRequestResponse? Кроме того, как вы вызываете его при запуске?
    Глен Ф.

    1
    @GlenF. Дело в том, что не имеет значения, как выглядит CreateChatRequestResponse. это может быть любой POCO. это вызывается в вашем стартапе. Вы можете просто вызвать его при запуске приложения либо в файле StartUp.cs, либо в Global.asax.
    Мэтт М

    Возможно, я совершенно не прав, но если только CreateChatRequestResponseэто не будет заменено тем, Tкак это будет проходить через все объекты Entity. Пожалуйста, поправьте меня, если я ошибаюсь.
    Fwd079

    0

    Простое решение проблемы, которую пытается решить 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)));
    
    Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
    Licensed under cc by-sa 3.0 with attribution required.