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


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.