Ordnen Sie Spaltennamen manuell mit Klasseneigenschaften zu

173

Ich bin neu im Dapper Micro ORM. Bisher kann ich es für einfache ORM-bezogene Dinge verwenden, aber ich kann die Namen der Datenbankspalten nicht den Klasseneigenschaften zuordnen.

Zum Beispiel habe ich die folgende Datenbanktabelle:

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

und ich habe eine Klasse namens Person:

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

Bitte beachten Sie, dass sich meine Spaltennamen in der Tabelle vom Eigenschaftsnamen der Klasse unterscheiden, der ich die Daten zuordnen möchte, die ich aus dem Abfrageergebnis erhalten habe.

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

Der obige Code funktioniert nicht, da die Spaltennamen nicht mit den Eigenschaften des Objekts (Person) übereinstimmen. Kann ich in diesem Szenario in Dapper etwas tun, um person_id => PersonIddie Spaltennamen mit Objekteigenschaften manuell zuzuordnen (z. B. )?

user1154985
quelle

Antworten:

80

Das funktioniert gut:

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 hat keine Funktion, mit der Sie ein Spaltenattribut angeben können. Ich bin nicht dagegen, Unterstützung dafür hinzuzufügen, vorausgesetzt, wir ziehen die Abhängigkeit nicht ein.

Sam Safran
quelle
@ Sam Saffron gibt es eine Möglichkeit, den Tabellenalias anzugeben. Ich habe eine Klasse namens Country, aber in der Datenbank hat die Tabelle aufgrund der Konventionen für die architektonische Benennung einen sehr komplizierten Namen.
TheVillageIdiot
64
Das Spaltenattribut ist praktisch, um die Ergebnisse gespeicherter Prozeduren abzubilden.
Ronnie Overby
2
Spaltenattribute sind auch nützlich, um eine enge physische und / oder semantische Kopplung zwischen Ihrer Domäne und den Details der Toolimplementierung, die Sie zur Materialisierung Ihrer Entitäten verwenden, einfacher zu ermöglichen. Fügen Sie daher keine Unterstützung dafür hinzu !!!! :)
Derek Greer
Ich verstehe nicht, warum columnattribe nicht da ist, wenn tableattribute. Wie würde dieses Beispiel mit Einfügungen, Aktualisierungen und SPs funktionieren? Ich würde gerne columnattribe sehen, es ist absolut einfach und würde die Migration von anderen Lösungen, die etwas Ähnliches wie das jetzt nicht mehr existierende linq-sql implementieren, sehr einfach machen.
Vman
197

Dapper unterstützt jetzt benutzerdefinierte Zuordnungen von Spalten zu Eigenschaften. Dies geschieht über die ITypeMap- Schnittstelle. Dapper stellt eine CustomPropertyTypeMap- Klasse bereit, die den größten Teil dieser Arbeit erledigen kann. Beispielsweise:

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

Und das Modell:

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

Es ist wichtig zu beachten, dass die Implementierung von CustomPropertyTypeMap erfordert, dass das Attribut vorhanden ist und mit einem der Spaltennamen übereinstimmt, da sonst die Eigenschaft nicht zugeordnet wird. Die DefaultTypeMap- Klasse bietet die Standardfunktionalität und kann genutzt werden, um dieses Verhalten zu ändern:

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

Auf diese Weise wird es einfach, einen benutzerdefinierten Typ-Mapper zu erstellen, der die Attribute automatisch verwendet, wenn sie vorhanden sind, ansonsten aber auf das Standardverhalten zurückgreift:

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

Das heißt, wir können jetzt problemlos Typen unterstützen, für die eine Zuordnung erforderlich ist, indem wir Attribute verwenden:

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

Hier ist eine Zusammenfassung des vollständigen Quellcodes .

Kaleb Pederson
quelle
Ich habe mit dem gleichen Problem zu kämpfen ... und dies scheint der Weg zu sein, den ich gehen sollte ... Ich bin ziemlich verwirrt darüber, wo dieser Code "Dapper.SqlMapper.SetTypeMap (typeof (MyModel)" heißen würde. neuer ColumnAttributeTypeMapper <MyModel> ()); " stackoverflow.com/questions/14814972/…
Rohan Büchner
Sie sollten es einmal aufrufen, bevor Sie Fragen stellen. Sie können dies beispielsweise in einem statischen Konstruktor tun, da dieser nur einmal aufgerufen werden muss.
Kaleb Pederson
7
Empfehlen Sie, dies zur offiziellen Antwort zu machen - diese Funktion von Dapper ist äußerst nützlich.
Killthrush
3
Die von @Oliver ( stackoverflow.com/a/34856158/364568 ) veröffentlichte Mapping-Lösung funktioniert und erfordert weniger Code.
Riga
4
Ich liebe es, wie das Wort "leicht" so mühelos herumgeworfen wird: P
Jonathan B.
80

Für einige Zeit sollte Folgendes funktionieren:

Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;
Marc Gravell
quelle
6
Obwohl dies nicht wirklich die Antwort auf die Frage " Spaltennamen manuell mit Klasseneigenschaften zuordnen" ist, ist es für mich viel besser, als manuell zuzuordnen (leider ist es in PostgreSQL besser, Unterstriche in Spaltennamen zu verwenden). Bitte entfernen Sie die Option MatchNamesWithUnderscores in den nächsten Versionen nicht! Danke dir!!!
Victorvartan
5
@ Victororvartan Es gibt keine Pläne, die MatchNamesWithUnderscoresOption zu entfernen . Wenn wir die Konfigurations-API überarbeiten würden, würde ich das Mitglied bestenfallsMatchNamesWithUnderscores an Ort und Stelle belassen (was im Idealfall immer noch funktioniert) und einen [Obsolete]Marker hinzufügen , um Personen auf die neue API zu verweisen.
Marc Gravell
4
@MarcGravell Die Worte "Für einige Zeit" am Anfang Ihrer Antwort haben mich beunruhigt, dass Sie sie in einer zukünftigen Version entfernen könnten, danke für die Klarstellung! Und ein großes Dankeschön für Dapper, ein wunderbares Mikro-ORM, das ich gerade für ein kleines Projekt zusammen mit Npgsql auf ASP.NET Core verwendet habe!
Victorvartan
2
Dies ist leicht die beste Antwort. Ich habe Stapel und Stapel von Umgehungsmöglichkeiten gefunden, bin aber schließlich darauf gestoßen. Einfach die beste, aber am wenigsten beworbene Antwort.
teaMonkeyFruit
29

Hier ist eine einfache Lösung, für die keine Attribute erforderlich sind, sodass Sie den Infrastrukturcode aus Ihren POCOs heraushalten können.

Dies ist eine Klasse, die sich mit den Zuordnungen befasst. Ein Wörterbuch würde funktionieren, wenn Sie alle Spalten zugeordnet hätten. Mit dieser Klasse können Sie jedoch nur die Unterschiede angeben. Darüber hinaus enthält es umgekehrte Zuordnungen, sodass Sie das Feld aus der Spalte und die Spalte aus dem Feld abrufen können. Dies kann hilfreich sein, wenn Sie beispielsweise SQL-Anweisungen generieren.

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

Richten Sie das ColumnMap-Objekt ein und weisen Sie Dapper an, das Mapping zu verwenden.

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
quelle
Dies ist eine gute Lösung, wenn Sie in Ihrem POCO grundsätzlich eine Nichtübereinstimmung der Eigenschaften mit dem haben, was Ihre Datenbank beispielsweise von einer gespeicherten Prozedur zurückgibt.
Crush
1
Ich mag die Prägnanz, die die Verwendung eines Attributs bietet, aber konzeptionell ist diese Methode sauberer - sie koppelt Ihren POCO nicht an Datenbankdetails.
Bruno Brant
Wenn ich Dapper richtig verstehe, hat es keine bestimmte Insert () -Methode, sondern nur eine Execute () ... würde dieser Mapping-Ansatz für Einfügungen funktionieren? Oder Updates? Danke
UuDdLrLrSs
29

Ich mache folgendes mit Dynamic und 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
quelle
12

Eine einfache Möglichkeit, dies zu erreichen, besteht darin, nur Aliase für die Spalten in Ihrer Abfrage zu verwenden. Wenn Ihre Datenbankspalte PERSON_IDund die Eigenschaft Ihres Objekts ist ID, können Sie dies einfach select PERSON_ID as Id ...in Ihrer Abfrage tun, und Dapper wird es wie erwartet abholen.

Brad Westness
quelle
12

Entnommen aus den Dapper-Tests, die derzeit auf Dapper 1.42 laufen.

// custom mapping
var map = new CustomPropertyTypeMap(typeof(TypeWithMapping), 
                                    (type, columnName) => type.GetProperties().FirstOrDefault(prop => GetDescriptionFromAttribute(prop) == columnName));
Dapper.SqlMapper.SetTypeMap(typeof(TypeWithMapping), map);

Hilfsklasse, um den Namen aus dem Description-Attribut zu entfernen (ich persönlich habe Column wie das @ kalebs-Beispiel verwendet)

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

Klasse

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

   [Description("A")]
   public string B { get; set; }
}
Oliver
quelle
1
Damit es auch für Eigenschaften arbeiten , wo keine Beschreibung definiert ist, änderte ich die Rückkehr GetDescriptionFromAttributezu return (attrib?.Description ?? member.Name).ToLower();und hinzugefügt , .ToLower()um columnNamein der Karte nicht case sensitive sein sollte.
Sam White
11

Das Spielen mit dem Mapping ist eine Grenzlinie, die sich in echtes ORM-Land bewegt. Anstatt damit zu kämpfen und Dapper in seiner wirklich einfachen (schnellen) Form zu halten, ändern Sie Ihr SQL einfach leicht wie folgt:

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

Führen Sie diesen Code für jede Ihrer Poco-Klassen aus, bevor Sie die Verbindung zu Ihrer Datenbank öffnen:

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

Fügen Sie dann die Datenanmerkungen wie folgt zu Ihren Poco-Klassen hinzu:

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

Danach sind Sie fertig. Machen Sie einfach einen Abfrageanruf, so etwas wie:

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
quelle
1
Alle Eigenschaften müssen über das Spaltenattribut verfügen. Gibt es eine Möglichkeit, eine Eigenschaft zuzuordnen, falls der Mapper nicht verfügbar ist?
Sandeep.gosavi
5

Wenn Sie .NET 4.5.1 oder höher verwenden, überprüfen Sie Dapper.FluentColumnMapping für die Zuordnung des LINQ-Stils. Damit können Sie die Datenbankzuordnung vollständig von Ihrem Modell trennen (keine Anmerkungen erforderlich).

mamuesstack
quelle
5
Ich bin der Autor von Dapper.FluentColumnMapping. Die Trennung der Zuordnungen von den Modellen war eines der primären Entwurfsziele. Ich wollte den zentralen Datenzugriff (dh Repository-Schnittstellen, Modellobjekte usw.) von den datenbankspezifischen konkreten Implementierungen isolieren, um eine saubere Trennung der Bedenken zu erreichen. Vielen Dank für die Erwähnung und ich bin froh, dass Sie es nützlich fanden! :-)
Alexander
github.com/henkmollema/Dapper-FluentMap ist ähnlich. Sie benötigen jedoch kein Paket von Drittanbietern mehr. Dapper hat Dapper.SqlMapper hinzugefügt. Weitere Informationen finden Sie in meiner Antwort, wenn Sie interessiert sind.
Tadej
4

Dies ist ein Huckepack von anderen Antworten. Es ist nur ein Gedanke, den ich hatte, um die Abfragezeichenfolgen zu verwalten.

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-Methode

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

Für alle, die Dapper 1.12 verwenden, müssen Sie Folgendes tun, um dies zu erreichen:

  • Fügen Sie eine neue Spaltenattributklasse hinzu:

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

  • Suche nach dieser Zeile:

    map = new DefaultTypeMap(type);

    und kommentiere es aus.

  • Schreiben Sie dies stattdessen:

            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
    quelle
    Ich bin mir nicht sicher, ob ich das verstehe. Empfehlen Sie Benutzern, Dapper zu ändern, um die Attributzuordnung nach Spalten zu ermöglichen? In diesem Fall ist es möglich, den oben angegebenen Code zu verwenden, ohne Änderungen an Dapper vorzunehmen.
    Kaleb Pederson
    1
    Aber dann müssen Sie die Zuordnungsfunktion für jeden Ihrer Modelltypen aufrufen, nicht wahr? Ich bin an einer generischen Lösung interessiert, damit alle meine Typen das Attribut verwenden können, ohne die Zuordnung für jeden Typ aufrufen zu müssen.
    Uri Abramson
    2
    Ich würde gerne sehen, dass DefaultTypeMap mit einem Strategiemuster implementiert wird, das aus dem von @UriAbramson erwähnten Grund ersetzt werden kann. Siehe code.google.com/p/dapper-dot-net/issues/detail?id=140
    Richard Collette
    1

    Die Lösung von Kaleb Pederson hat bei mir funktioniert. Ich habe den ColumnAttributeTypeMapper aktualisiert, um ein benutzerdefiniertes Attribut zuzulassen (für das zwei verschiedene Zuordnungen für dasselbe Domänenobjekt erforderlich waren), und die Eigenschaften aktualisiert, um private Setter in Fällen zuzulassen, in denen ein Feld abgeleitet werden musste und die Typen unterschiedlich waren.

    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
    quelle
    1

    Ich weiß, dass dies ein relativ alter Thread ist, aber ich dachte, ich würde das, was ich getan habe, da draußen werfen.

    Ich wollte, dass die Attributzuordnung global funktioniert. Entweder stimmen Sie mit dem Eigenschaftsnamen überein (auch bekannt als Standard), oder Sie stimmen mit einem Spaltenattribut für die Klasseneigenschaft überein. Ich wollte dies auch nicht für jede einzelne Klasse einrichten müssen, der ich zugeordnet war. Aus diesem Grund habe ich eine DapperStart-Klasse erstellt, die ich beim Start der App aufrufe:

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

    Ziemlich einfach. Ich bin mir nicht sicher, auf welche Probleme ich noch stoßen werde, als ich das gerade geschrieben habe, aber es funktioniert.

    Matt M.
    quelle
    Wie sieht CreateChatRequestResponse aus? Wie rufen Sie es beim Start auf?
    Glen F.
    1
    @ GlenF. Der Punkt ist, dass es keine Rolle spielt, wie CreateChatRequestResponse aussieht. es kann jeder POCO sein. Dies wird bei Ihrem Start aufgerufen. Sie können es einfach beim Start Ihrer App entweder in Ihrer StartUp.cs oder in Ihrer Global.asax aufrufen.
    Matt M
    Vielleicht irre ich mich völlig, aber es CreateChatRequestResponsesei denn, es wird ersetzt, Twie dies alle Entitätsobjekte durchlaufen würde. Bitte korrigieren Sie mich, wenn ich falsch liege.
    Fwd079
    0

    Die einfache Lösung für das Problem, das Kaleb zu lösen versucht, besteht darin, nur den Eigenschaftsnamen zu akzeptieren, wenn das Spaltenattribut nicht vorhanden ist:

    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
    quelle