Mappare manualmente i nomi delle colonne con le proprietà della classe


173

Sono nuovo nel micro ORM di Dapper. Finora sono in grado di usarlo per semplici cose relative a ORM ma non sono in grado di mappare i nomi delle colonne del database con le proprietà della classe.

Ad esempio, ho la seguente tabella di database:

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

e ho una classe chiamata Person:

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

Si noti che i nomi delle mie colonne nella tabella sono diversi dal nome della proprietà della classe a cui sto provando a mappare i dati che ho ottenuto dal risultato della query.

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

Il codice sopra riportato non funzionerà poiché i nomi delle colonne non corrispondono alle proprietà (Person) dell'oggetto. In questo scenario, c'è qualcosa che posso fare in Dapper per mappare manualmente (ad esempio person_id => PersonId) i nomi delle colonne con le proprietà degli oggetti?


Risposte:


80

Funziona bene:

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 non ha alcuna funzione che ti permetta di specificare un attributo di colonna , non sono contrario all'aggiunta di supporto per esso, a condizione che non tiriamo la dipendenza.


@Sam Saffron esiste un modo per specificare l'alias di tabella. Ho una classe chiamata Country ma nel db la tabella ha un nome molto contorto a causa delle convenzioni di denominazione archiche.
TheVillageIdiot

64
L'attributo della colonna sarebbe utile per mappare i risultati della procedura memorizzata.
Ronnie Overby,

2
Gli attributi di colonna sarebbero utili anche per facilitare più facilmente l'accoppiamento fisico e / o semantico stretto tra il tuo dominio e i dettagli di implementazione dello strumento che stai utilizzando per materializzare le tue entità. Pertanto, non aggiungere supporto per questo !!!! :)
Derek Greer,

Non capisco perché columnattribe non sia presente quando il tableattribute. Come funzionerebbe questo esempio con inserti, aggiornamenti e SP? Vorrei vedere columnattribe, è semplice e renderebbe la vita molto facile migrare da altre soluzioni che implementano qualcosa di simile come il defunto linq-sql.
Vman,

197

Dapper ora supporta colonne personalizzate per mappatori di proprietà. Lo fa attraverso l' interfaccia ITypeMap . Una classe CustomPropertyTypeMap è fornita da Dapper che può svolgere gran parte di questo lavoro. Per esempio:

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

E il modello:

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

È importante notare che l'implementazione di CustomPropertyTypeMap richiede che l'attributo esista e corrisponda a uno dei nomi di colonna o che la proprietà non sia mappata. La classe DefaultTypeMap fornisce le funzionalità standard e può essere sfruttata per modificare questo comportamento:

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

E con quello in atto, diventa facile creare un mappatore di tipo personalizzato che utilizzerà automaticamente gli attributi se sono presenti ma altrimenti tornerà al comportamento standard:

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

Ciò significa che ora possiamo facilmente supportare tipi che richiedono la mappa usando gli attributi:

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

Ecco un riassunto del codice sorgente completo .


Ho avuto problemi con questo stesso problema ... e questo sembra il percorso che dovrei seguire ... Sono abbastanza confuso su dove questo codice verrebbe chiamato "Dapper.SqlMapper.SetTypeMap (typeof (MyModel), new ColumnAttributeTypeMapper <MyModel> ()); " stackoverflow.com/questions/14814972/…
Rohan Büchner

Ti consigliamo di chiamarlo una volta prima di fare qualsiasi domanda. Potresti farlo in un costruttore statico, ad esempio, poiché deve essere chiamato solo una volta.
Kaleb Pederson,

7
Consiglia di rendere questa la risposta ufficiale: questa funzionalità di Dapper è estremamente utile.
killthrush,

3
Soluzione di mappatura pubblicato da @Oliver ( stackoverflow.com/a/34856158/364568 ) funziona e richiede meno codice.
Riga,

4
Adoro il modo in cui la parola "facilmente" viene lanciata in modo così semplice: P
Jonathan B.

80

Per qualche tempo, dovrebbe funzionare quanto segue:

Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;

6
Sebbene questa non sia davvero la risposta alla domanda " Mappare manualmente i nomi delle colonne con le proprietà della classe", per me è molto meglio che dover mappare manualmente (sfortunatamente in PostgreSQL è meglio usare i trattini bassi nei nomi delle colonne). Non rimuovere l'opzione MatchNamesWithUnderscores nelle prossime versioni! Grazie!!!
victorvartan,

5
@victorvartan non ci sono piani per rimuovere l' MatchNamesWithUnderscoresopzione. Nel migliore dei casi , se rifattorizzassimo l'API di configurazione, lascerei il MatchNamesWithUnderscoresmembro in posizione (che funziona ancora, idealmente) e aggiungere un [Obsolete]marcatore per indirizzare le persone alla nuova API.
Marc Gravell

4
@MarcGravell le parole "Per qualche tempo" all'inizio della tua risposta mi hanno preoccupato che potresti rimuoverlo in una versione futura, grazie per il chiarimento! E un grande grazie per Dapper, un meraviglioso micro ORM che ho appena iniziato a utilizzare per un piccolo progetto insieme a Npgsql su ASP.NET Core!
victorvartan,

2
Questa è facilmente la risposta migliore. Ho trovato pile e pile di maglie da lavoro, ma alla fine ci siamo imbattuti in questo. Facilmente la risposta migliore ma meno pubblicizzata.
teaMonkeyFruit

29

Ecco una soluzione semplice che non richiede attributi che ti consentono di mantenere il codice dell'infrastruttura fuori dai tuoi POCO.

Questa è una classe per gestire i mapping. Un dizionario funzionerebbe se si associassero tutte le colonne, ma questa classe consente di specificare solo le differenze. Inoltre, include mappe inverse in modo da poter ottenere il campo dalla colonna e la colonna dal campo, il che può essere utile quando si fanno cose come la generazione di istruzioni 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;
        }
    }
}

Imposta l'oggetto ColumnMap e di 'a Dapper di usare la mappatura.

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

Questa è una buona soluzione quando fondamentalmente si ha una discrepanza di proprietà nel proprio POCO a ciò che il database sta tornando, ad esempio da una procedura memorizzata.
schiaccia

1
In un certo senso mi piace la concisione data dall'uso di un attributo, ma concettualmente questo metodo è più pulito: non accoppia il tuo POCO ai dettagli del database.
Bruno Brant,

Se capisco correttamente Dapper, non ha un metodo Insert () specifico, solo un Execute () ... questo approccio di mappatura funzionerebbe per gli inserimenti? O aggiornamenti? Grazie
UuDdLrLrSs il

29

Faccio quanto segue usando dynamic e 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

Un modo semplice per raggiungere questo obiettivo è utilizzare solo gli alias nelle colonne della query. Se la colonna del tuo database è PERSON_IDe la proprietà del tuo oggetto è IDche puoi semplicemente fare la select PERSON_ID as Id ...tua query e Dapper lo prenderà come previsto.


12

Tratto dai test Dapper, attualmente su 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);

Classe di supporto per ottenere il nome dall'attributo Descrizione (personalmente ho usato la colonna come esempio @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;
}

Classe

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

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

2
Al fine di farlo funzionare anche per le proprietà in cui descrizione viene definito, ho cambiato il ritorno di GetDescriptionFromAttributeal return (attrib?.Description ?? member.Name).ToLower();e aggiunto .ToLower()a columnNamenella mappa non dovrebbe essere case sensitive.
Sam White,

11

Fare casini con la mappatura è un confine che si sposta in una vera terra ORM. Invece di combattere con esso e mantenere Dapper nella sua vera forma semplice (veloce), modifica leggermente il tuo SQL in questo modo:

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

8

Prima di aprire la connessione al tuo database, esegui questo pezzo di codice per ciascuna delle tue classi 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))));

Quindi aggiungi le annotazioni dei dati alle tue classi poco come questa:

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

Dopodiché, sei pronto. Basta effettuare una chiamata di query, qualcosa del tipo:

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
È necessario che tutte le proprietà abbiano l'attributo Column. Esiste un modo per mappare con proprietà nel caso in cui mapper non sia disponibile?
sandeep.gosavi,

5

Se stai usando .NET 4.5.1 o versioni successive, controlla Dapper.FluentColumnMapping per mappare lo stile LINQ. Ti consente di separare completamente la mappatura db dal tuo modello (non sono necessarie annotazioni)


5
Sono l'autore di Dapper.FluentColumnMapping. Separare le mappature dai modelli era uno degli obiettivi di progettazione principali. Volevo isolare l'accesso principale ai dati (ovvero interfacce di repository, oggetti modello, ecc.) Dalle implementazioni concrete specifiche del database per una netta separazione delle preoccupazioni. Grazie per la menzione e sono felice che l'abbia trovata utile! :-)
Alexander

github.com/henkmollema/Dapper-FluentMap è simile. Ma non hai più bisogno di un pacchetto di terze parti. Dapper ha aggiunto Dapper.SqlMapper. Vedi la mia risposta per maggiori dettagli se sei interessato.
Tadej,

4

Questo è il sostegno alle spalle di altre risposte. È solo un pensiero che ho avuto per la gestione delle stringhe di query.

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

Metodo API

using (var conn = ConnectionFactory.GetConnection())
{
    var person = conn.Query<Person>(Person.Select()).ToList();
    return person;
}

1

per tutti coloro che usano Dapper 1.12, ecco cosa devi fare per farlo:

  • Aggiungi una nuova classe di attributi di colonna:

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

  • Cerca questa linea:

    map = new DefaultTypeMap(type);

    e commentalo.

  • Scrivi questo invece:

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


  • Non sono sicuro di capire - stai raccomandando che gli utenti cambino Dapper per rendere possibile la mappatura degli attributi per colonne? In tal caso, è possibile utilizzare il codice che ho pubblicato sopra senza apportare modifiche a Dapper.
    Kaleb Pederson,

    1
    Ma poi dovrai chiamare la funzione di mappatura per ognuno dei tuoi tipi di modello, vero? sono interessato a una soluzione generica in modo che tutti i miei tipi possano utilizzare l'attributo senza dover chiamare la mappatura per ogni tipo.
    Uri Abramson,

    2
    Mi piacerebbe vedere DefaultTypeMap essere implementato usando un modello di strategia in modo tale che possa essere sostituito per il motivo menzionato da @UriAbramson. Vedi code.google.com/p/dapper-dot-net/issues/detail?id=140
    Richard Collette,

    1

    La soluzione di Kaleb Pederson ha funzionato per me. Ho aggiornato ColumnAttributeTypeMapper per consentire un attributo personalizzato (avevo il requisito per due diversi mapping sullo stesso oggetto di dominio) e ho aggiornato le proprietà per consentire ai setter privati ​​nei casi in cui un campo doveva essere derivato e i tipi differivano.

    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

    So che questo è un thread relativamente vecchio, ma ho pensato di buttare quello che ho fatto là fuori.

    Volevo che la mappatura degli attributi funzionasse a livello globale. O abbini il nome della proprietà (aka default) o abbini un attributo di colonna sulla proprietà della classe. Inoltre non volevo impostare questo per ogni singola classe a cui stavo mappando. Come tale, ho creato una classe DapperStart che invoco all'avvio dell'app:

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

    Abbastanza semplice. Non sono sicuro di quali problemi incontrerò ancora mentre ho appena scritto questo, ma funziona.


    Che aspetto ha CreateChatRequestResponse? Inoltre, come lo stai invocando all'avvio?
    Glen F.

    1
    @GlenF. il punto è che non ha importanza l'aspetto di CreateChatRequestResponse. può essere qualsiasi POCO. questo viene invocato nella tua startup. Puoi semplicemente invocarlo sull'avvio della tua app in StartUp.cs o Global.asax.
    Matt M

    Forse mi sbaglio completamente, ma a meno che non CreateChatRequestResponsevenga sostituito da Tcome ciò ripeterebbe attraverso tutti gli oggetti Entità. Perfavore, correggimi se sbaglio.
    Fwd079,

    0

    La semplice soluzione al problema che Kaleb sta cercando di risolvere è solo accettare il nome della proprietà se l'attributo della colonna non esiste:

    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)));
    
    Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
    Licensed under cc by-sa 3.0 with attribution required.