Вручну зіставити назви стовпців із властивостями класу


173

Я новачок у 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) імена стовпців із властивостями об’єкта?


Відповіді:


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, але в db таблиця має дуже звичну назву через архівні угоди про іменування.
TheVillageIdiot

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

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

Я не розумію, чому Columnattribe не існує, коли tableattribute. Як би цей приклад працював із вставками, оновленнями та 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>());

Ось історія повного вихідного коду .


Я боровся з цим самим питанням ... і це здається маршрутом, яким я мушу їхати ... Я дуже розгублений щодо того, де цей код буде називатися "Dapper.SqlMapper.SetTypeMap (typeof (MyModel)", новий ColumnAttributeTypeMapper <MyModel> ()); " stackoverflow.com/questions/14814972/…
Рохан Бюхнер

Ви хочете зателефонувати йому один раз, перш ніж робити будь-які запити. Наприклад, ви можете це зробити в статичному конструкторі, оскільки його потрібно викликати лише один раз.
Калеб Педерсон

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

3
Mapping рішення опубліковано @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, які зараз на 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);

Клас помічників, щоб отримати ім'я від атрибуту Опис (я особисто використовував стовпець, як приклад @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; }
}

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

11

Возитися з картографуванням - це переміщення кордону в реальну землю ORM. Замість того, щоб боротися з ним і зберігати 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))));

Потім додайте примітки до даних у ваші класи такої категорії:

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. Це дозволяє повністю відокремити відображення db від вашої моделі (немає необхідності в анотаціях)


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/isissue/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.