Wie schreibe ich eine zu viele Abfragen in Dapper.Net?

79

Ich habe diesen Code geschrieben, um eine bis viele Beziehungen zu projizieren, aber er funktioniert nicht:

using (var connection = new SqlConnection(connectionString))
{
   connection.Open();

   IEnumerable<Store> stores = connection.Query<Store, IEnumerable<Employee>, Store>
                        (@"Select Stores.Id as StoreId, Stores.Name, 
                                  Employees.Id as EmployeeId, Employees.FirstName,
                                  Employees.LastName, Employees.StoreId 
                           from Store Stores 
                           INNER JOIN Employee Employees ON Stores.Id = Employees.StoreId",
                        (a, s) => { a.Employees = s; return a; }, 
                        splitOn: "EmployeeId");

   foreach (var store in stores)
   {
       Console.WriteLine(store.Name);
   }
}

Kann jemand den Fehler erkennen?

BEARBEITEN:

Dies sind meine Entitäten:

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public double Price { get; set; }
    public IList<Store> Stores { get; set; }

    public Product()
    {
        Stores = new List<Store>();
    }
}

public class Store
{
    public int Id { get; set; }
    public string Name { get; set; }
    public IEnumerable<Product> Products { get; set; }
    public IEnumerable<Employee> Employees { get; set; }

    public Store()
    {
        Products = new List<Product>();
        Employees = new List<Employee>();
    }
}

BEARBEITEN:

Ich ändere die Abfrage in:

IEnumerable<Store> stores = connection.Query<Store, List<Employee>, Store>
        (@"Select Stores.Id as StoreId ,Stores.Name,Employees.Id as EmployeeId,
           Employees.FirstName,Employees.LastName,Employees.StoreId 
           from Store Stores INNER JOIN Employee Employees 
           ON Stores.Id = Employees.StoreId",
         (a, s) => { a.Employees = s; return a; }, splitOn: "EmployeeId");

und ich werde Ausnahmen los! Mitarbeiter werden jedoch überhaupt nicht zugeordnet. Ich bin mir immer noch nicht sicher, mit welchem ​​Problem es bei der IEnumerable<Employee>ersten Abfrage war.

TCM
quelle
1
Wie sehen Ihre Entitäten aus?
Gideon
2
Wie funktioniert das nicht? Bekommst du eine Ausnahme? Unerwartete Ergebnisse?
Driis
1
Der Fehler ist nicht aussagekräftig, deshalb habe ich mich nicht darum gekümmert, ihn zu posten. Ich erhalte: "{" Wert kann nicht null sein. \ R \ nParametername: con "}". Die Zeile, die in SqlMapper einen Fehler auslöst, lautet: "il.Emit (OpCodes.Newobj, type.GetConstructor (BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null));"
TCM

Antworten:

160

Dieser Beitrag zeigt, wie Sie eine stark normalisierte SQL-Datenbank abfragen und das Ergebnis einer Reihe hoch verschachtelter C # POCO-Objekte zuordnen.

Zutaten:

  • 8 Zeilen C #.
  • Einige ziemlich einfache SQL, die einige Joins verwendet.
  • Zwei großartige Bibliotheken.

Die Einsicht, die es mir ermöglichte, dieses Problem zu lösen, besteht darin, das MicroORMvon zu trennen mapping the result back to the POCO Entities. Daher verwenden wir zwei separate Bibliotheken:

Im Wesentlichen verwenden wir Dapper , um die Datenbank abzufragen, und dann Slapper.Automapper , um das Ergebnis direkt in unsere POCOs abzubilden.

Vorteile

  • Einfachheit . Es sind weniger als 8 Codezeilen. Ich finde das viel einfacher zu verstehen, zu debuggen und zu ändern.
  • Weniger Code . Ein paar Codezeilen sind alles Slapper.Automapper muss alles verarbeiten, was Sie darauf werfen, selbst wenn wir einen komplexen verschachtelten POCO haben (dh POCO enthält, List<MyClass1>der wiederum enthält List<MySubClass2>, usw.).
  • Geschwindigkeit . Beide Bibliotheken verfügen über ein außerordentliches Maß an Optimierung und Caching, sodass sie fast so schnell ausgeführt werden wie handgestimmte ADO.NET-Abfragen.
  • Trennung von Bedenken . Wir können das MicroORM gegen ein anderes austauschen, und das Mapping funktioniert immer noch und umgekehrt.
  • Flexibilität . Slapper.Automapper verarbeitet beliebig verschachtelte Hierarchien und ist nicht auf einige Verschachtelungsebenen beschränkt. Wir können leicht schnelle Änderungen vornehmen, und alles wird noch funktionieren.
  • Debuggen . Wir können zuerst sehen, dass die SQL-Abfrage ordnungsgemäß funktioniert, und dann überprüfen, ob das Ergebnis der SQL-Abfrage ordnungsgemäß den POCO-Zielentitäten zugeordnet ist.
  • Einfache Entwicklung in SQL . Ich finde, dass das Erstellen abgeflachter Abfragen mit inner joins, um flache Ergebnisse zurückzugeben, viel einfacher ist als das Erstellen mehrerer select-Anweisungen mit Stitching auf der Clientseite.
  • Optimierte Abfragen in SQL . In einer stark normalisierten Datenbank ermöglicht das Erstellen einer flachen Abfrage der SQL-Engine, erweiterte Optimierungen auf das Ganze anzuwenden, die normalerweise nicht möglich wären, wenn viele kleine Einzelabfragen erstellt und ausgeführt würden.
  • Vertrauen . Dapper ist das Backend für StackOverflow, und Randy Burden ist ein bisschen ein Superstar. Muss ich noch mehr sagen?
  • Entwicklungsgeschwindigkeit. Ich war in der Lage, einige außerordentlich komplexe Abfragen mit vielen Verschachtelungsebenen durchzuführen, und die Entwicklungszeit war ziemlich gering.
  • Weniger Bugs. Ich habe es einmal geschrieben, es hat einfach funktioniert, und diese Technik hilft jetzt, ein FTSE-Unternehmen mit Strom zu versorgen. Es gab so wenig Code, dass es kein unerwartetes Verhalten gab.

Nachteile

  • Skalierung über 1.000.000 Zeilen zurückgegeben. Funktioniert gut, wenn <100.000 Zeilen zurückgegeben werden. Wenn wir jedoch mehr als 1.000.000 Zeilen zurückbringen, sollten wir den Datenverkehr zwischen uns und dem SQL Server nicht reduzieren inner join(wodurch Duplikate zurückgebracht werden), sondern stattdessen mehrere selectAnweisungen verwenden und alles wieder zusammenfügen Client-Seite (siehe die anderen Antworten auf dieser Seite).
  • Diese Technik ist abfrageorientiert . Ich habe diese Technik nicht zum Schreiben in die Datenbank verwendet, aber ich bin sicher, dass Dapper dies mit etwas mehr Arbeit mehr als tun kann, da StackOverflow selbst Dapper als Data Access Layer (DAL) verwendet.

Leistungstest

In meinen Tests hat Slapper.Automapper den von Dapper zurückgegebenen Ergebnissen einen kleinen Overhead hinzugefügt, was bedeutete, dass es immer noch 10x schneller als Entity Framework war und die Kombination immer noch ziemlich nahe an der theoretischen Höchstgeschwindigkeit liegt, zu der SQL + C # fähig ist .

In den meisten praktischen Fällen würde der größte Teil des Overheads in einer nicht optimalen SQL-Abfrage liegen und nicht mit einer gewissen Zuordnung der Ergebnisse auf der C # -Seite.

Leistungstestergebnisse

Gesamtzahl der Iterationen: 1000

  • Dapper by itself: 1,889 Millisekunden pro Abfrage mit 3 lines of code to return the dynamic.
  • Dapper + Slapper.Automapper: 2,463 Millisekunden pro Abfrage unter Verwendung einer zusätzlichen 3 lines of code for the query + mapping from dynamic to POCO Entities.

Gearbeitetes Beispiel

In diesem Beispiel haben wir eine Liste von Contactsund jeder Contactkann eine oder mehrere haben phone numbers.

POCO-Einheiten

public class TestContact
{
    public int ContactID { get; set; }
    public string ContactName { get; set; }
    public List<TestPhone> TestPhones { get; set; }
}

public class TestPhone
{
    public int PhoneId { get; set; }
    public int ContactID { get; set; } // foreign key
    public string Number { get; set; }
}

SQL-Tabelle TestContact

Geben Sie hier die Bildbeschreibung ein

SQL-Tabelle TestPhone

Beachten Sie, dass diese Tabelle einen Fremdschlüssel hat, ContactIDder sich auf die TestContactTabelle bezieht (dies entspricht dem List<TestPhone>im obigen POCO).

Geben Sie hier die Bildbeschreibung ein

SQL, das ein flaches Ergebnis erzeugt

In unserer SQL-Abfrage verwenden wir so viele JOINAnweisungen, wie wir benötigen, um alle benötigten Daten in einer flachen, denormalisierten Form abzurufen . Ja, dies kann zu Duplikaten in der Ausgabe führen, aber diese Duplikate werden automatisch entfernt, wenn wir Slapper.Automapper verwenden , um das Ergebnis dieser Abfrage automatisch direkt in unsere POCO-Objektzuordnung abzubilden.

USE [MyDatabase];
    SELECT tc.[ContactID] as ContactID
          ,tc.[ContactName] as ContactName
          ,tp.[PhoneId] AS TestPhones_PhoneId
          ,tp.[ContactId] AS TestPhones_ContactId
          ,tp.[Number] AS TestPhones_Number
          FROM TestContact tc
    INNER JOIN TestPhone tp ON tc.ContactId = tp.ContactId

Geben Sie hier die Bildbeschreibung ein

C # -Code

const string sql = @"SELECT tc.[ContactID] as ContactID
          ,tc.[ContactName] as ContactName
          ,tp.[PhoneId] AS TestPhones_PhoneId
          ,tp.[ContactId] AS TestPhones_ContactId
          ,tp.[Number] AS TestPhones_Number
          FROM TestContact tc
    INNER JOIN TestPhone tp ON tc.ContactId = tp.ContactId";

string connectionString = // -- Insert SQL connection string here.

using (var conn = new SqlConnection(connectionString))
{
    conn.Open();    
    // Can set default database here with conn.ChangeDatabase(...)
    {
        // Step 1: Use Dapper to return the  flat result as a Dynamic.
        dynamic test = conn.Query<dynamic>(sql);

        // Step 2: Use Slapper.Automapper for mapping to the POCO Entities.
        // - IMPORTANT: Let Slapper.Automapper know how to do the mapping;
        //   let it know the primary key for each POCO.
        // - Must also use underscore notation ("_") to name parameters in the SQL query;
        //   see Slapper.Automapper docs.
        Slapper.AutoMapper.Configuration.AddIdentifiers(typeof(TestContact), new List<string> { "ContactID" });
        Slapper.AutoMapper.Configuration.AddIdentifiers(typeof(TestPhone), new List<string> { "PhoneID" });

        var testContact = (Slapper.AutoMapper.MapDynamic<TestContact>(test) as IEnumerable<TestContact>).ToList();      

        foreach (var c in testContact)
        {                               
            foreach (var p in c.TestPhones)
            {
                Console.Write("ContactName: {0}: Phone: {1}\n", c.ContactName, p.Number);   
            }
        }
    }
}

Ausgabe

Geben Sie hier die Bildbeschreibung ein

POCO-Entitätshierarchie

In Visual Studio können wir sehen, dass Slapper.Automapper unsere POCO-Entitäten ordnungsgemäß ausgefüllt hat, dh wir haben eine List<TestContact>und jede TestContacthat eine List<TestPhone>.

Geben Sie hier die Bildbeschreibung ein

Anmerkungen

Sowohl Dapper als auch Slapper.Automapper speichern alles intern, um die Geschwindigkeit zu erhöhen. Wenn Sie auf Speicherprobleme stoßen (sehr unwahrscheinlich), stellen Sie sicher, dass Sie gelegentlich den Cache für beide leeren.

Stellen Sie sicher, dass Sie die zurückkommenden Spalten mit der Unterstrich- _Notation ( ) benennen, um Slapper.Automapper Hinweise zu geben, wie das Ergebnis den POCO-Entitäten zugeordnet werden kann.

Stellen Sie sicher, dass Sie Slapper.Automapper-Hinweise zum Primärschlüssel für jede POCO-Entität geben (siehe Zeilen Slapper.AutoMapper.Configuration.AddIdentifiers). Sie können dies auch Attributesauf dem POCO verwenden. Wenn Sie diesen Schritt überspringen, kann dies (theoretisch) schief gehen, da Slapper.Automapper nicht weiß, wie das Mapping ordnungsgemäß ausgeführt wird.

Update 14.06.2015

Diese Technik wurde erfolgreich auf eine riesige Produktionsdatenbank mit über 40 normalisierten Tabellen angewendet. Es funktionierte perfekt eine erweiterte SQL - Abfrage mit mehr als 16 abzubilden inner joinund left joinin die richtige POCO Hierarchie (mit 4 Ebenen der Verschachtelung). Die Abfragen sind unglaublich schnell, fast so schnell wie die manuelle Codierung in ADO.NET (normalerweise waren es 52 Millisekunden für die Abfrage und 50 Millisekunden für die Zuordnung vom flachen Ergebnis zur POCO-Hierarchie). Dies ist wirklich nichts Revolutionäres, aber es übertrifft Entity Framework in Bezug auf Geschwindigkeit und Benutzerfreundlichkeit, insbesondere wenn wir nur Abfragen ausführen.

Update 19.02.2016

Code läuft seit 9 Monaten einwandfrei in der Produktion. Die neueste Version von Slapper.Automapperenthält alle Änderungen, die ich vorgenommen habe, um das Problem zu beheben, das mit der Rückgabe von Nullen in der SQL-Abfrage zusammenhängt.

Update 2017-02-20

Code läuft seit 21 Monaten einwandfrei in der Produktion und hat fortlaufende Anfragen von Hunderten von Benutzern in einem FTSE 250-Unternehmen bearbeitet.

Slapper.Automappereignet sich auch hervorragend zum Zuordnen einer CSV-Datei direkt zu einer Liste von POCOs. Lesen Sie die CSV-Datei in eine IDictionary-Liste und ordnen Sie sie dann direkt der Zielliste der POCOs zu. Der einzige Trick besteht darin, dass Sie eine Eigenschaft hinzufügen int Id {get; set}und sicherstellen müssen , dass sie für jede Zeile eindeutig ist (andernfalls kann der Automapper nicht zwischen den Zeilen unterscheiden).

Update 2019-01-29

Kleinere Aktualisierung, um weitere Codekommentare hinzuzufügen.

Siehe: https://github.com/SlapperAutoMapper/Slapper.AutoMapper

Contango
quelle
1
Ich mag die Präfixkonvention für Tabellennamen in all Ihrem SQL nicht wirklich, sie unterstützt jedoch nicht etwas wie Dappers "splitOn"?
Knochen
3
Diese Tabellennamenkonvention wird von Slapper.Automapper benötigt. Ja, Dapper unterstützt die direkte Zuordnung zu POCOs, aber ich bevorzuge die Verwendung von Slapper.Automapper, da der Code so sauber und wartbar ist.
Contango
2
Ich denke, ich würde Slapper verwenden, wenn Sie nicht alle Spalten aliasen müssten - stattdessen möchte ich in Ihrem Beispiel sagen können :, splitOn: "PhoneId" - wäre das nicht ziemlich viel einfacher als alles aliasen zu müssen?
Knochen
1
Ich mag das Aussehen von Slapper wirklich und frage mich nur, ob Sie einen Link-Join versucht haben, wenn eine Person keine Kontaktnummern hat? Haben Sie eine gute Möglichkeit, damit umzugehen?
Nicht geliebt
1
@tbone splitOn enthält keine Informationen darüber, wo in Ihrem Objekt dieses Element hingehört, weshalb Slapper einen Pfad wie diesen verwendet
Nicht geliebt
20

Ich wollte es so einfach wie möglich halten, meine Lösung:

public List<ForumMessage> GetForumMessagesByParentId(int parentId)
{
    var sql = @"
    select d.id_data as Id, d.cd_group As GroupId, d.cd_user as UserId, d.tx_login As Login, 
        d.tx_title As Title, d.tx_message As [Message], d.tx_signature As [Signature], d.nm_views As Views, d.nm_replies As Replies, 
        d.dt_created As CreatedDate, d.dt_lastreply As LastReplyDate, d.dt_edited As EditedDate, d.tx_key As [Key]
    from 
        t_data d
    where d.cd_data = @DataId order by id_data asc;

    select d.id_data As DataId, di.id_data_image As DataImageId, di.cd_image As ImageId, i.fl_local As IsLocal
    from 
        t_data d
        inner join T_data_image di on d.id_data = di.cd_data
        inner join T_image i on di.cd_image = i.id_image 
    where d.id_data = @DataId and di.fl_deleted = 0 order by d.id_data asc;";

    var mapper = _conn.QueryMultiple(sql, new { DataId = parentId });
    var messages = mapper.Read<ForumMessage>().ToDictionary(k => k.Id, v => v);
    var images = mapper.Read<ForumMessageImage>().ToList();

    foreach(var imageGroup in images.GroupBy(g => g.DataId))
    {
        messages[imageGroup.Key].Images = imageGroup.ToList();
    }

    return messages.Values.ToList();
}

Ich rufe immer noch einmal die Datenbank auf, und während ich jetzt 2 Abfragen anstelle von einer ausführe, verwendet die zweite Abfrage einen INNER-Join anstelle eines weniger optimalen LEFT-Joins.

Davy
quelle
5
Ich mag diesen Ansatz. Reine adrette und meiner Meinung nach verständlichere Zuordnung.
Avner
1
Es scheint so, als wäre es einfach, eine Erweiterungsmethode zu verwenden, die ein paar Albmdas benötigt, eine für die Schlüsselauswahl und eine für die untergeordnete Auswahl. Ähnlich wie .Join(, erzeugt jedoch ein Objektdiagramm anstelle eines abgeflachten Ergebnisses.
AaronLS
8

Eine geringfügige Änderung von Andrews Antwort, bei der ein Func verwendet wird, um den übergeordneten Schlüssel anstelle von auszuwählen GetHashCode.

public static IEnumerable<TParent> QueryParentChild<TParent, TChild, TParentKey>(
    this IDbConnection connection,
    string sql,
    Func<TParent, TParentKey> parentKeySelector,
    Func<TParent, IList<TChild>> childSelector,
    dynamic param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null)
{
    Dictionary<TParentKey, TParent> cache = new Dictionary<TParentKey, TParent>();

    connection.Query<TParent, TChild, TParent>(
        sql,
        (parent, child) =>
            {
                if (!cache.ContainsKey(parentKeySelector(parent)))
                {
                    cache.Add(parentKeySelector(parent), parent);
                }

                TParent cachedParent = cache[parentKeySelector(parent)];
                IList<TChild> children = childSelector(cachedParent);
                children.Add(child);
                return cachedParent;
            },
        param as object, transaction, buffered, splitOn, commandTimeout, commandType);

    return cache.Values;
}

Anwendungsbeispiel

conn.QueryParentChild<Product, Store, int>("sql here", prod => prod.Id, prod => prod.Stores)
Lehm
quelle
Bei dieser Lösung ist zu beachten, dass Ihre Elternklasse für die Instanziierung der untergeordneten Eigenschaft verantwortlich ist. class Parent { public List<Child> Children { get; set; } public Parent() { this.Children = new List<Child>(); } }
Clay
1
Diese Lösung ist ausgezeichnet und hat für uns funktioniert. Ich musste eine Überprüfung mit der child.add hinzufügen, um nach null zu suchen, falls keine untergeordneten Zeilen zurückgegeben wurden.
Tlbignerd
7

Nach dieser Antwort ist in Dapper.Net niemand zu viele Mapping-Unterstützung integriert. Abfragen geben immer ein Objekt pro Datenbankzeile zurück. Es gibt jedoch eine alternative Lösung.

Damir Arh
quelle
Es tut mir leid, aber ich verstehe nicht, wie ich das in meiner Abfrage verwende. Es wird versucht, die Datenbank zweimal ohne Verknüpfungen abzufragen (und im Beispiel eine fest codierte 1 zu verwenden). Im Beispiel wird nur 1 Hauptentität zurückgegeben, die wiederum untergeordnete Entitäten enthält. In meinem Fall möchte ich den Join projizieren (Liste, die intern eine Liste enthält). Wie mache ich das mit dem von Ihnen erwähnten Link? In dem Link, in dem in der Zeile steht: (contact, phones) => { contact.Phones = phones; } Ich müsste einen Filter für Telefone schreiben, deren Kontakt-ID mit der Kontakt-ID des Kontakts übereinstimmt. Das ist ziemlich ineffizient.
TCM
@Anthony Schau dir Mikes Antwort an. Er führt eine einzelne Abfrage mit zwei Ergebnismengen aus und verbindet sie anschließend mit der Map-Methode. Natürlich müssen Sie den Wert in Ihrem Fall nicht fest codieren. Ich werde versuchen, in ein paar Stunden ein Beispiel zusammenzustellen.
Damir Arh
1
Okay, ich habe es endlich zum Laufen gebracht. Vielen Dank! Ich weiß nicht, wie sich dies auf die Leistung der Abfrage der Datenbank zweimal auswirken würde, was mit einem einzelnen Join erreicht werden könnte.
TCM
2
Ich verstehe auch nicht, welche Änderungen ich vornehmen müsste, wenn es 3 Tabellen gegeben hätte: p
TCM
1
das ist total beschissen .. warum um alles in der Welt vermeiden Sie Verbindungen?
GorillaApe
2

Hier ist eine grobe Problemumgehung

    public static IEnumerable<TOne> Query<TOne, TMany>(this IDbConnection cnn, string sql, Func<TOne, IList<TMany>> property, dynamic param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null)
    {
        var cache = new Dictionary<int, TOne>();
        cnn.Query<TOne, TMany, TOne>(sql, (one, many) =>
                                            {
                                                if (!cache.ContainsKey(one.GetHashCode()))
                                                    cache.Add(one.GetHashCode(), one);

                                                var localOne = cache[one.GetHashCode()];
                                                var list = property(localOne);
                                                list.Add(many);
                                                return localOne;
                                            }, param as object, transaction, buffered, splitOn, commandTimeout, commandType);
        return cache.Values;
    }

Es ist keineswegs der effizienteste Weg, aber es wird Sie zum Laufen bringen. Ich werde versuchen, dies zu optimieren, wenn ich eine Chance bekomme.

benutze es so:

conn.Query<Product, Store>("sql here", prod => prod.Stores);

Denken Sie daran, dass Ihre Objekte implementiert werden müssen GetHashCode, vielleicht so:

    public override int GetHashCode()
    {
        return this.Id.GetHashCode();
    }
Andrew Bullock
quelle
11
Die Cache-Implementierung ist fehlerhaft. Hash-Codes sind nicht eindeutig - zwei Objekte können denselben Hash-Code haben. Dies kann dazu führen, dass eine Objektliste mit Elementen gefüllt wird, die zu einem anderen Objekt gehören.
stmax
2

Hier ist eine andere Methode:

Order (one) - OrderDetail (viele)

using (var connection = new SqlCeConnection(connectionString))
{           
    var orderDictionary = new Dictionary<int, Order>();

    var list = connection.Query<Order, OrderDetail, Order>(
        sql,
        (order, orderDetail) =>
        {
            Order orderEntry;

            if (!orderDictionary.TryGetValue(order.OrderID, out orderEntry))
            {
                orderEntry = order;
                orderEntry.OrderDetails = new List<OrderDetail>();
                orderDictionary.Add(orderEntry.OrderID, orderEntry);
            }

            orderEntry.OrderDetails.Add(orderDetail);
            return orderEntry;
        },
        splitOn: "OrderDetailID")
    .Distinct()
    .ToList();
}

Quelle : http://dapper-tutorial.net/result-multi-mapping#example---query-multi-mapping-one-to-many

Exocomp
quelle