Ręcznie mapuj nazwy kolumn z właściwościami klas

173

Jestem nowy w Dapper micro ORM. Do tej pory mogę go używać do prostych rzeczy związanych z ORM, ale nie jestem w stanie zmapować nazw kolumn bazy danych z właściwościami klasy.

Na przykład mam następującą tabelę bazy danych:

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

i mam klasę o nazwie Osoba:

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

Zwróć uwagę, że nazwy moich kolumn w tabeli różnią się od nazwy właściwości klasy, do której próbuję zmapować dane, które otrzymałem z wyniku zapytania.

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

Powyższy kod nie zadziała, ponieważ nazwy kolumn nie są zgodne z właściwościami obiektu (Person). Czy w tym scenariuszu jest coś, co mogę zrobić w Dapper, aby ręcznie mapować (np. person_id => PersonId) Nazwy kolumn z właściwościami obiektu?

user1154985
źródło
możliwy duplikat Dappera.
Mapuj

Odpowiedzi:

80

To działa dobrze:

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 nie ma funkcji, która pozwala określić atrybut kolumny , nie jestem przeciwny dodawaniu jej obsługi, pod warunkiem, że nie pobieramy zależności.

Sam Saffron
źródło
@Sam Saffron jest w jakikolwiek sposób mogę określić alias tabeli. Mam klasę o nazwie Country, ale w db tabela ma bardzo zawiłą nazwę ze względu na archiczne konwencje nazewnictwa.
TheVillageIdiot
64
Atrybut kolumny byłby przydatny do mapowania wyników procedury składowanej.
Ronnie Overby
2
Atrybuty kolumn byłyby również przydatne do łatwiejszego ułatwienia ścisłego fizycznego i / lub semantycznego powiązania między domeną a szczegółami implementacji narzędzia, których używasz do materializacji swoich jednostek. Dlatego nie dodawaj obsługi tego !!!! :)
Derek Greer
Nie rozumiem, dlaczego columnattribe nie występuje, gdy tableattribute. Jak ten przykład działałby z wstawkami, aktualizacjami i dodatkami? Chciałbym zobaczyć columnattribe, jego martwe proste i bardzo ułatwiłoby życie migracji z innych rozwiązań, które implementują coś podobnego, jak nieistniejący już linq-sql.
Vman
197

Dapper obsługuje teraz niestandardowe kolumny do mapowania właściwości. Robi to za pośrednictwem interfejsu ITypeMap . CustomPropertyTypeMap klasa jest przez Dapper że można zrobić większość tej pracy. Na przykład:

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

A model:

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

Należy zauważyć, że implementacja CustomPropertyTypeMap wymaga, aby atrybut istniał i pasował do jednej z nazw kolumn, w przeciwnym razie właściwość nie zostanie zmapowana. DefaultTypeMap klasa zapewnia standardową funkcjonalność i może być użyta do zmiany tego zachowania:

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

Dzięki temu łatwo jest utworzyć niestandardowy program odwzorowujący typy, który automatycznie użyje atrybutów, jeśli są obecne, ale w przeciwnym razie powróci do standardowego zachowania:

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

Oznacza to, że możemy teraz łatwo obsługiwać typy, które wymagają mapowania przy użyciu atrybutów:

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

Oto streszczenie pełnego kodu źródłowego .

Kaleb Pederson
źródło
Zmagałem się z tym samym problemem ... i wygląda na to, że jest to trasa, którą powinienem podążać ... Jestem dość zdezorientowany, gdzie ten kod zostałby nazwany „Dapper.SqlMapper.SetTypeMap (typeof (MyModel), new ColumnAttributeTypeMapper <MyModel> ()); " stackoverflow.com/questions/14814972/…
Rohan Büchner
Będziesz chciał zadzwonić do tego raz przed wykonaniem jakichkolwiek zapytań. Możesz to zrobić na przykład w konstruktorze statycznym, ponieważ wystarczy go wywołać tylko raz.
Kaleb Pederson
7
Polecam uczynić to oficjalną odpowiedzią - ta funkcja Dappera jest niezwykle przydatna.
killthrush
3
Rozwiązanie mapowe opublikowane przez @Oliver ( stackoverflow.com/a/34856158/364568 ) działa i wymaga mniej kodu.
Ryga
4
Uwielbiam to, jak słowo „łatwo” jest rzucane bez wysiłku: P
Jonathan B.
80

Przez jakiś czas powinny działać:

Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;
Marc Gravell
źródło
6
Chociaż tak naprawdę nie jest to odpowiedź na pytanie „ Ręcznie mapuj nazwy kolumn z właściwościami klas”, dla mnie jest to o wiele lepsze niż ręczne mapowanie (niestety w PostgreSQL lepiej jest używać podkreślenia w nazwach kolumn). Proszę nie usuwać opcji MatchNamesWithUnderscores w następnych wersjach! Dziękuję Ci!!!
victorvartan,
5
@victorvartan nie ma planów usunięcia tej MatchNamesWithUnderscoresopcji. W najlepszym przypadku, gdybyśmy zmienili konfigurację API, zostawiłbym MatchNamesWithUnderscoresczłonka na miejscu (to nadal działa, idealnie) i dodałbym [Obsolete]znacznik, aby wskazać ludziom nowe API.
Marc Gravell
4
@MarcGravell słowa „Od jakiegoś czasu” na początku Twojej odpowiedzi zmartwiły mnie, że możesz ją usunąć w przyszłej wersji, dzięki za wyjaśnienie! I wielkie dzięki za Dapper, wspaniały mikro ORM, którego właśnie zacząłem używać w małym projekcie wraz z Npgsql na ASP.NET Core!
victorvartan,
2
To z pewnością najlepsza odpowiedź. Znalazłem stosy pracy, ale w końcu się na to natknąłem. Z łatwością najlepsza, ale najmniej reklamowana odpowiedź.
teaMonkeyFruit
29

Oto proste rozwiązanie, które nie wymaga atrybutów, które pozwalają utrzymać kod infrastruktury z dala od POCO.

To jest klasa do obsługi mapowań. Słownik działałby, gdybyś zmapował wszystkie kolumny, ale ta klasa umożliwia określenie tylko różnic. Ponadto zawiera odwrócone mapy, dzięki czemu można pobrać pole z kolumny i kolumnę z pola, co może być przydatne podczas wykonywania takich czynności, jak generowanie instrukcji 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;
        }
    }
}

Skonfiguruj obiekt ColumnMap i powiedz Dapperowi, aby użył mapowania.

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])));
Randall Sutton
źródło
Jest to dobre rozwiązanie, gdy zasadniczo masz niezgodność właściwości w swoim POCO z tym, co zwraca baza danych, na przykład z procedury składowanej.
zmiażdżyć
1
Podoba mi się zwięzłość, jaką daje użycie atrybutu, ale koncepcyjnie ta metoda jest czystsza - nie łączy twojego POCO ze szczegółami bazy danych.
Bruno Brant
Jeśli dobrze rozumiem Dapper, nie ma on określonej metody Insert (), tylko Execute () ... czy to podejście do mapowania zadziała w przypadku wstawień? Albo aktualizacje? Dzięki
UuDdLrLrSs
29

Wykonuję następujące czynności przy użyciu dynamicznego i 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;
    }
liorafar
źródło
12

Prostym sposobem osiągnięcia tego jest użycie aliasów w kolumnach w zapytaniu. Jeśli twoja kolumna bazy danych jest PERSON_IDi obiekt ma właściwość ID, możesz po prostu wykonać select PERSON_ID as Id ...zapytanie, a Dapper odbierze go zgodnie z oczekiwaniami.

Brad Westness
źródło
12

Zaczerpnięte z Dapper Tests, które są obecnie na 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);

Klasa pomocnicza, aby uzyskać nazwę z atrybutu Description (osobiście użyłem kolumny jak przykład @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;
}

Klasa

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

   [Description("A")]
   public string B { get; set; }
}
Oliver
źródło
1
Aby działał nawet dla właściwości, w których nie jest zdefiniowany opis, zmieniłem powrót GetDescriptionFromAttributedo return (attrib?.Description ?? member.Name).ToLower();i dodałem .ToLower()do columnNamena mapie, aby nie uwzględniać wielkości liter.
Sam White
11

Zamieszanie z mapowaniem to graniczne przejście do prawdziwej krainy ORM. Zamiast walczyć z nim i utrzymywać Dapper w jego prawdziwej prostej (szybkiej) formie, po prostu zmodyfikuj swój SQL w taki sposób:

var sql = @"select top 1 person_id as PersonId,FirstName,LastName from Person";
mxmissile
źródło
8

Zanim otworzysz połączenie z bazą danych, wykonaj ten fragment kodu dla każdej z klas 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))));

Następnie dodaj adnotacje danych do swoich klas poco w następujący sposób:

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

Potem wszystko jest gotowe. Po prostu wykonaj zapytanie, na przykład:

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();
}
Tadej
źródło
1
Wszystkie właściwości muszą mieć atrybut Column. Czy istnieje sposób na mapowanie z właściwością, jeśli program mapujący nie jest dostępny?
sandeep.gosavi
5

Jeśli używasz .NET 4.5.1 lub nowszego, wyewidencjonuj Dapper.FluentColumnMapping do mapowania stylu LINQ. Pozwala w pełni oddzielić mapowanie db od modelu (bez adnotacji)

mamuesstack
źródło
5
Jestem autorem Dapper.FluentColumnMapping. Oddzielenie mapowań od modeli było jednym z głównych celów projektowych. Chciałem odizolować dostęp do podstawowych danych (tj. Interfejsy repozytorium, obiekty modeli, itp.) Od konkretnych implementacji specyficznych dla bazy danych, aby zapewnić czyste oddzielenie problemów. Dzięki za wzmiankę i cieszę się, że okazało się przydatne! :-)
Alexander
github.com/henkmollema/Dapper-FluentMap jest podobny. Ale nie potrzebujesz już pakietu innej firmy. Dapper dodał Dapper.SqlMapper. Zobacz moją odpowiedź, aby uzyskać więcej informacji, jeśli jesteś zainteresowany.
Tadej
4

To jest świnka wycofująca się z innych odpowiedzi. To tylko myśl, którą miałem do zarządzania ciągami zapytań.

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

Metoda API

using (var conn = ConnectionFactory.GetConnection())
{
    var person = conn.Query<Person>(Person.Select()).ToList();
    return person;
}
christo8989
źródło
1

dla wszystkich, którzy używają Dapper 1.12, oto, co musisz zrobić, aby to zrobić:

  • Dodaj nową klasę atrybutów kolumn:

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

  • Wyszukaj tę linię:

    map = new DefaultTypeMap(type);

    i skomentuj to.

  • Zamiast tego napisz:

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

  • Uri Abramson
    źródło
    Nie jestem pewien, czy rozumiem - czy zalecasz użytkownikom zmianę Dappera, aby umożliwić mapowanie atrybutów według kolumn? Jeśli tak, jest to możliwe przy użyciu kodu, który zamieściłem powyżej, bez wprowadzania zmian w Dapper.
    Kaleb Pederson
    1
    Ale wtedy będziesz musiał wywołać funkcję mapowania dla każdego z twoich typów modeli, prawda? Jestem zainteresowany rozwiązaniem ogólnym, aby wszystkie moje typy mogły używać atrybutu bez konieczności wywoływania mapowania dla każdego typu.
    Uri Abramson
    2
    Chciałbym, aby DefaultTypeMap został zaimplementowany przy użyciu wzorca strategii, tak aby można go było zastąpić z powodu, o którym wspomina @UriAbramson. Zobacz code.google.com/p/dapper-dot-net/issues/detail?id=140
    Richard Collette,
    1

    Rozwiązanie Kaleba Pedersona zadziałało dla mnie. Zaktualizowałem ColumnAttributeTypeMapper, aby zezwolić na niestandardowy atrybut (wymagał dwóch różnych mapowań w tym samym obiekcie domeny) i zaktualizowałem właściwości, aby umożliwić ustawianie prywatne w przypadkach, gdy trzeba było wyprowadzić pole, a typy różniły się.

    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))
                })
        {
            //
        }
    }
    GameSalutes
    źródło
    1

    Wiem, że to stosunkowo stary wątek, ale pomyślałem, że wyrzucę tam to, co zrobiłem.

    Chciałem, aby mapowanie atrybutów działało globalnie. Albo dopasujesz nazwę właściwości (aka domyślną), albo dopasujesz atrybut kolumny do właściwości klasy. Nie chciałem też konfigurować tego dla każdej klasy, do której mapowałem. W związku z tym utworzyłem klasę DapperStart, którą wywołuję przy starcie aplikacji:

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

    Dość proste. Nie jestem pewien, jakie problemy napotkam, gdy właśnie to napisałem, ale działa.

    Matt M.
    źródło
    Jak wygląda CreateChatRequestResponse? Jak wywołujesz to w startupie?
    Glen F.
    1
    @GlenF. chodzi o to, że nie ma znaczenia, jak wygląda CreateChatRequestResponse. może to być dowolny POCO. to jest wywoływane w twoim startupie. Możesz po prostu wywołać go na początku swojej aplikacji w swoim StartUp.cs lub Global.asax.
    Matt M
    Być może całkowicie się mylę, ale chyba że CreateChatRequestResponsezostanie zastąpione przez to, w Tjaki sposób iteruje przez wszystkie obiekty Entity. Proszę, popraw mnie jeśli się mylę.
    Fwd079
    0

    Prostym rozwiązaniem problemu, który Kaleb próbuje rozwiązać, jest po prostu zaakceptowanie nazwy właściwości, jeśli atrybut kolumny nie istnieje:

    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)));
    
    Stewart Cunningham
    źródło