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

Я новичок в 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) имена столбцов со свойствами объекта?


person user1154985    schedule 17.01.2012    source источник
comment
возможный дубликат Dapper. Сопоставление со столбцом SQL с пробелами в именах столбцов   -  person David McEleney    schedule 30.06.2015


Ответы (17)


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

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

person Sam Saffron    schedule 18.01.2012
comment
@Sam Saffron, я могу указать псевдоним таблицы. У меня есть класс с именем Country, но в базе данных таблица имеет очень запутанное имя из-за архаичных соглашений об именах. - person TheVillageIdiot; 07.08.2012
comment
Атрибут столбца был бы удобен для сопоставления результатов хранимой процедуры. - person Ronnie Overby; 02.11.2012
comment
Атрибуты столбцов также могут быть полезны для облегчения тесной физической и/или семантической связи между вашим доменом и деталями реализации инструмента, которые вы используете для материализации своих сущностей. Поэтому не добавляйте поддержку для этого!!!! :) - person Derek Greer; 26.06.2014
comment
Я не понимаю, почему атрибут столбца отсутствует, когда атрибут таблицы. Как этот пример будет работать со вставками, обновлениями и SP? Я хотел бы видеть columnattribe, он очень прост и упростит миграцию с других решений, которые реализуют что-то похожее, например, ныне несуществующий linq-sql. - person Vman; 10.05.2020

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

Вот Краткий обзор полного исходного кода.

person Kaleb Pederson    schedule 27.09.2012
comment
Я боролся с этой же проблемой... и это похоже на маршрут, по которому я должен идти... Я совершенно не понимаю, где этот код будет называться Dapper.SqlMapper.SetTypeMap(typeof(MyModel), new ColumnAttributeTypeMapper‹MyModel›()); stackoverflow.com/questions/14814972/ - person Rohan Büchner; 12.02.2013
comment
Вы захотите вызвать его один раз, прежде чем делать какие-либо запросы. Вы можете сделать это, например, в статическом конструкторе, так как его нужно вызвать только один раз. - person Kaleb Pederson; 14.02.2013
comment
Рекомендую сделать это официальным ответом — эта функция Dapper чрезвычайно полезна. - person killthrush; 17.04.2013
comment
Нет, это не входит в dapper. Я отредактировал сообщение, добавив ссылку на суть с полным рабочим кодом. - person Kaleb Pederson; 25.04.2013
comment
Мне любопытно, почему резервная поддержка не включена. Мое предположение было бы проблемой ухудшения скорости. - person Karl Kieninger; 30.04.2015
comment
Решение для сопоставления, опубликованное @Oliver (stackoverflow.com/a/34856158/364568), работает и требует меньше кода. - person Riga; 18.04.2018
comment
Мне нравится, как легко бросается слово легко :P - person Jonathan B.; 23.07.2018

Некоторое время должно работать следующее:

Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;
person Marc Gravell    schedule 30.12.2015
comment
Хотя на самом деле это не ответ на вопрос Вручную сопоставлять имена столбцов со свойствами класса, для меня это намного лучше, чем вручную (к сожалению, в PostgreSQL лучше использовать символы подчеркивания в именах столбцов). Пожалуйста, не удаляйте опцию MatchNamesWithUnderscores в следующих версиях! Спасибо!!! - person victorvartan; 10.08.2016
comment
@victorvartan нет планов удалять опцию MatchNamesWithUnderscores. В лучшем случае, если бы мы реорганизовали API конфигурации, я бы оставил член MatchNamesWithUnderscores на месте (в идеале он все еще работает) и добавил бы маркер [Obsolete], чтобы указать людям на новый API. - person Marc Gravell; 11.08.2016
comment
@MarcGravell слова В течение некоторого времени в начале вашего ответа я беспокоился, что вы можете удалить его в будущей версии, спасибо за разъяснение! И большое спасибо за Dapper, замечательный микро ORM, который я только начал использовать для крошечного проекта вместе с Npgsql на ASP.NET Core! - person victorvartan; 11.08.2016
comment
Это легко лучший ответ. Я нашел кучу и кучу обходных путей, но, наконец, наткнулся на это. Легко лучший, но наименее разрекламированный ответ. - person teaMonkeyFruit; 26.11.2019

Я делаю следующее, используя динамический и 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;
    }
person liorafar    schedule 25.01.2017
comment
Лучшее решение оттуда - person MiBol; 16.07.2021

Вот простое решение, не требующее атрибутов, позволяющее не допускать попадания кода инфраструктуры в ваши 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])));
person Randall Sutton    schedule 19.09.2014
comment
Это хорошее решение, когда у вас в основном есть несоответствие свойств в вашем POCO тому, что ваша база данных возвращает, например, из хранимой процедуры. - person crush; 26.09.2014
comment
Мне нравится лаконичность, которую дает использование атрибута, но концептуально этот метод чище — он не связывает ваш POCO с данными базы данных. - person Bruno Brant; 24.08.2018
comment
Если я правильно понимаю Dapper, у него нет специального метода Insert(), только Execute()... будет ли этот подход сопоставления работать для вставок? Или обновления? Спасибо - person StayOnTarget; 26.09.2018

Взято из тестов Dapper, которые в настоящее время находится на 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 (лично я использовал столбец, например пример @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 Oliver    schedule 18.01.2016
comment
Чтобы он работал даже для свойств, для которых не определено описание, я изменил возврат GetDescriptionFromAttribute на return (attrib?.Description ?? member.Name).ToLower(); и добавил .ToLower() в columnName на карте, это не должно быть чувствительным к регистру. - person Sam White; 30.11.2018
comment
Спасибо. Есть ли способ установить сопоставление для каждого вызова SQL, а не глобально? Мне это нужно только для использования половины моих вызовов. - person Lukas; 19.01.2021

Простой способ добиться этого — просто использовать псевдонимы для столбцов в запросе.

Если ваш столбец базы данных равен PERSON_ID, а свойство вашего объекта равно ID, вы можете просто сделать

select PERSON_ID as Id ...

в вашем запросе, и Dapper подберет его, как и ожидалось.

person Brad Westness    schedule 12.05.2015

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

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

Прежде чем открывать соединение с базой данных, выполните этот фрагмент кода для каждого из ваших 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();
}
person Tadej    schedule 02.08.2017
comment
Все свойства должны иметь атрибут Column. Есть ли способ сопоставить свойство, если картограф недоступен? - person sandeep.gosavi; 14.05.2018

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

person mamuesstack    schedule 03.06.2016
comment
Я автор Dapper.FluentColumnMapping. Отделение отображений от моделей было одной из основных целей проектирования. Я хотел изолировать доступ к основным данным (т. е. интерфейсы репозитория, объекты модели и т. д.) от конкретных реализаций, специфичных для базы данных, для четкого разделения задач. Спасибо за упоминание, и я рад, что вы нашли это полезным! :-) - person Alexander; 03.06.2017
comment
github.com/henkmollema/Dapper-FluentMap аналогичен. Но вам больше не нужен сторонний пакет. Dapper добавил Dapper.SqlMapper. Смотрите мой ответ для более подробной информации, если вы заинтересованы. - person Tadej; 02.08.2017

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

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;
}
person christo8989    schedule 17.01.2017

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

  • Add a new column attribute class:

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

  • Search for this line:

    map = new DefaultTypeMap(type);
    

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

  • Write this instead:

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

  • person Uri Abramson    schedule 25.04.2013
    comment
    Я не уверен, что понимаю - вы рекомендуете пользователям изменить Dapper, чтобы сделать возможным сопоставление атрибутов по столбцам? Если это так, то можно использовать код, который я разместил выше, без внесения изменений в Dapper. - person Kaleb Pederson; 25.04.2013
    comment
    Но тогда вам придется вызывать функцию сопоставления для каждого из ваших типов моделей, не так ли? меня интересует универсальное решение, чтобы все мои типы могли использовать атрибут без необходимости вызывать сопоставление для каждого типа. - person Uri Abramson; 25.04.2013
    comment
    Я хотел бы, чтобы DefaultTypeMap был реализован с использованием шаблона стратегии, чтобы его можно было заменить по причине, упомянутой @UriAbramson. См. code.google.com/p/dapper-dot. -net/issues/detail?id=140 - person Richard Collette; 11.06.2013

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

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

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

    person Matt M    schedule 19.10.2018
    comment
    Как выглядит CreateChatRequestResponse? Кроме того, как вы вызываете его при запуске? - person Glen F.; 15.02.2019
    comment
    @ГленФ. дело в том, что не имеет значения, как выглядит CreateChatRequestResponse. это может быть любой POCO. это вызывается в вашем запуске. Вы можете просто вызвать его при запуске приложения либо в StartUp.cs, либо в Global.asax. - person Matt M; 22.02.2019
    comment
    Возможно, я совершенно не прав, но если CreateChatRequestResponse не заменить на T, как это будет перебирать все объекты Entity. Пожалуйста, поправьте меня, если я ошибаюсь. - person Fawad Raza; 31.10.2019

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

    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)));
    
    
    person Stewart Cunningham    schedule 05.04.2019

    Обратите внимание, что сопоставление объектов 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();
    
    person Alexy Dumenigo Aguila    schedule 04.12.2020

    Более простой способ (такой же, как ответ @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;
           });
       };
    
    person CEPOCTb    schedule 07.12.2020

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