Wie ordne ich Listen verschachtelter Objekte mit Dapper zu?

126

Ich verwende derzeit Entity Framework für meinen Datenbankzugriff, möchte aber einen Blick auf Dapper werfen. Ich habe Klassen wie diese:

public class Course{
   public string Title{get;set;}
   public IList<Location> Locations {get;set;}
   ...
}

public class Location{
   public string Name {get;set;}
   ...
}

Ein Kurs kann also an mehreren Orten unterrichtet werden. Entity Framework übernimmt die Zuordnung für mich, sodass mein Kursobjekt mit einer Liste von Speicherorten gefüllt wird. Wie würde ich mit Dapper vorgehen, ist das überhaupt möglich oder muss ich es in mehreren Abfrageschritten tun?

b3n
quelle
Verwandte Frage: stackoverflow.com/questions/6379155/…
Jeroen K
Hier ist meine Lösung: stackoverflow.com/a/57395072/8526957
Sam Sch

Antworten:

56

Dapper ist kein ausgewachsener ORM, der keine magische Generierung von Abfragen und dergleichen verarbeitet.

Für Ihr spezielles Beispiel würde wahrscheinlich Folgendes funktionieren:

Schnapp dir die Kurse:

var courses = cnn.Query<Course>("select * from Courses where Category = 1 Order by CreationDate");

Holen Sie sich das entsprechende Mapping:

var mappings = cnn.Query<CourseLocation>(
   "select * from CourseLocations where CourseId in @Ids", 
    new {Ids = courses.Select(c => c.Id).Distinct()});

Besorgen Sie sich die relevanten Orte

var locations = cnn.Query<Location>(
   "select * from Locations where Id in @Ids",
   new {Ids = mappings.Select(m => m.LocationId).Distinct()}
);

Ordnen Sie alles zu

Überlassen Sie dies dem Leser, erstellen Sie einige Karten und durchlaufen Sie Ihre Kurse, die mit den Standorten gefüllt sind.

Vorsichtsmaßnahme Der inTrick funktioniert, wenn Sie weniger als 2100 Suchvorgänge (SQL Server) haben. Wenn Sie mehr haben, möchten Sie die Abfrage wahrscheinlich dahingehend ändern, select * from CourseLocations where CourseId in (select Id from Courses ... )dass Sie in diesem Fall auch alle Ergebnisse auf einmal ziehen könnenQueryMultiple

Sam Safran
quelle
Danke für die Klarstellung Sam. Wie Sie oben beschrieben haben, führe ich nur eine zweite Abfrage aus, um die Standorte abzurufen und sie manuell dem Kurs zuzuweisen. Ich wollte nur sicherstellen, dass ich nichts verpasse, was es mir ermöglicht, es mit einer Abfrage zu tun.
b3n
2
Sam, in einer großen Anwendung, in der Sammlungen regelmäßig auf Domänenobjekten verfügbar gemacht werden, wie im Beispiel, wo würden Sie empfehlen, dass sich dieser Code physisch befindet ? (Angenommen, Sie möchten eine ähnlich vollständig konstruierte [Course] -Entität an zahlreichen verschiedenen Stellen in Ihrem Code verwenden.) Im Konstruktor? In einer Klassenfabrik? Irgendwo anders?
Knochen
177

Alternativ können Sie eine Abfrage mit einer Suche verwenden:

var lookup = new Dictionary<int, Course>();
conn.Query<Course, Location, Course>(@"
    SELECT c.*, l.*
    FROM Course c
    INNER JOIN Location l ON c.LocationId = l.Id                    
    ", (c, l) => {
        Course course;
        if (!lookup.TryGetValue(c.Id, out course))
            lookup.Add(c.Id, course = c);
        if (course.Locations == null) 
            course.Locations = new List<Location>();
        course.Locations.Add(l); /* Add locations to course */
        return course;
     }).AsQueryable();
var resultList = lookup.Values;

Siehe hier https://www.tritac.com/blog/dappernet-by-example/

Jeroen K.
quelle
9
Das hat mir eine Menge Zeit gespart. Eine Änderung, die andere möglicherweise benötigen, ist das Argument splitOn:, da ich nicht die Standard-ID verwendet habe.
Bill Sambrone
1
Für LEFT JOIN erhalten Sie ein Nullelement in der Standortliste. Entfernen Sie sie mit var items = lookup.Values; items.ForEach (x => x.Locations.RemoveAll (y => y == null));
Choco Smith
Ich kann dies nur kompilieren, wenn ich am Ende von Zeile 1 ein Semikolon habe und das Komma vor 'AsQueryable ()' entferne. Ich würde die Antwort bearbeiten, aber 62 Upvoter vor mir schienen zu denken, dass es in Ordnung ist, vielleicht fehlt mir etwas ...
Bitcoder
1
Für LEFT JOIN: Sie müssen kein weiteres Foreach durchführen. Überprüfen Sie einfach, bevor Sie es hinzufügen: if (l! = Null) course.Locations.Add (l).
jpgrassi
1
Da Sie ein Wörterbuch verwenden. Wäre dies schneller, wenn Sie QueryMultiple verwenden und Kurs und Ort getrennt abfragen und dann dasselbe Wörterbuch verwenden würden, um dem Kurs einen Ort zuzuweisen? Es ist im Wesentlichen dasselbe abzüglich der inneren Verknüpfung, was bedeutet, dass SQL nicht so viele Bytes überträgt?
Mike
43

Kein lookupWörterbuch erforderlich

var coursesWithLocations = 
    conn.Query<Course, Location, Course>(@"
        SELECT c.*, l.*
        FROM Course c
        INNER JOIN Location l ON c.LocationId = l.Id                    
        ", (course, location) => {
            course.Locations = course.Locations ?? new List<Location>();
            course.Locations.Add(location); 
            return course;
        }).AsQueryable();
tchelidze
quelle
3
Das ist ausgezeichnet - dies sollte meiner Meinung nach die ausgewählte Antwort sein. Leute, die dies tun, achten jedoch darauf *, da dies die Leistung beeinträchtigen kann.
bis zum
2
Das einzige Problem dabei ist, dass Sie den Header in jedem Standortdatensatz duplizieren. Wenn es pro Kurs viele Standorte gibt, kann es zu einer erheblichen Datenverdoppelung kommen, die die Bandbreite erhöht, das Parsen / Zuordnen länger dauert und mehr Speicher zum Lesen all dessen verwendet.
Daniel Lorenz
10
Ich bin mir nicht sicher, ob das so funktioniert, wie ich es erwartet hatte. Ich habe 1 übergeordnetes Objekt mit 3 verwandten Objekten. Die Abfrage, die ich benutze, bekommt drei Zeilen zurück. die ersten Spalten, die das übergeordnete Element beschreiben und für jede Zeile dupliziert werden; Die Aufteilung auf ID würde jedes einzelne Kind identifizieren. Meine Ergebnisse sind 3 doppelte Eltern mit 3 Kindern .... sollte ein Elternteil mit 3 Kindern sein.
Topwik
2
@ Topwik ist richtig. es funktioniert auch bei mir nicht wie erwartet.
Maciej Pszczolinski
3
Ich hatte tatsächlich 3 Eltern, jeweils 1 Kind mit diesem Code. Ich bin mir nicht sicher, warum mein Ergebnis anders ist als @topwik, aber es funktioniert trotzdem nicht.
Th3Morg
29

Ich weiß, dass ich sehr spät dran bin, aber es gibt noch eine andere Option. Hier können Sie QueryMultiple verwenden. Etwas wie das:

var results = cnn.QueryMultiple(@"
    SELECT * 
      FROM Courses 
     WHERE Category = 1 
  ORDER BY CreationDate
          ; 
    SELECT A.*
          ,B.CourseId 
      FROM Locations A 
INNER JOIN CourseLocations B 
        ON A.LocationId = B.LocationId 
INNER JOIN Course C 
        ON B.CourseId = B.CourseId 
       AND C.Category = 1
");

var courses = results.Read<Course>();
var locations = results.Read<Location>(); //(Location will have that extra CourseId on it for the next part)
foreach (var course in courses) {
   course.Locations = locations.Where(a => a.CourseId == course.CourseId).ToList();
}
Daniel Lorenz
quelle
3
Eine Sache zu beachten. Wenn es viele Orte / Kurse gibt, sollten Sie die Orte einmal durchlaufen und sie in eine Wörterbuchsuche einfügen, damit Sie N log N anstelle von N ^ 2 Geschwindigkeit haben. Macht einen großen Unterschied bei größeren Datensätzen.
Daniel Lorenz
6

Tut mir leid, dass ich zu spät zur Party komme (wie immer). Für mich ist es in Bezug auf Leistung und Lesbarkeit einfacher, eine zu verwenden Dictionary, wie es Jeroen K getan hat. Um eine Multiplikation der Header über Standorte hinweg zu vermeiden Distinct(), entferne ich potenzielle Dups:

string query = @"SELECT c.*, l.*
    FROM Course c
    INNER JOIN Location l ON c.LocationId = l.Id";
using (SqlConnection conn = DB.getConnection())
{
    conn.Open();
    var courseDictionary = new Dictionary<Guid, Course>();
    var list = conn.Query<Course, Location, Course>(
        query,
        (course, location) =>
        {
            if (!courseDictionary.TryGetValue(course.Id, out Course courseEntry))
            {
                courseEntry = course;
                courseEntry.Locations = courseEntry.Locations ?? new List<Location>();
                courseDictionary.Add(courseEntry.Id, courseEntry);
            }

            courseEntry.Locations.Add(location);
            return courseEntry;
        },
        splitOn: "Id")
    .Distinct()
    .ToList();

    return list;
}
Francisco Tena
quelle
4

Etwas fehlt. Wenn Sie nicht jedes Feld Locationsin der SQL-Abfrage angeben , kann das Objekt Locationnicht gefüllt werden. Schau mal:

var lookup = new Dictionary<int, Course>()
conn.Query<Course, Location, Course>(@"
    SELECT c.*, l.Name, l.otherField, l.secondField
    FROM Course c
    INNER JOIN Location l ON c.LocationId = l.Id                    
    ", (c, l) => {
        Course course;
        if (!lookup.TryGetValue(c.Id, out course)) {
            lookup.Add(c.Id, course = c);
        }
        if (course.Locations == null) 
            course.Locations = new List<Location>();
        course.Locations.Add(a);
        return course;
     },
     ).AsQueryable();
var resultList = lookup.Values;

Mit l.*in der Abfrage, hatte ich die Liste der Standorte , aber ohne Daten.

Eduardo Pires
quelle
0

Ich bin mir nicht sicher, ob es jemand braucht, aber ich habe eine dynamische Version ohne Modell für eine schnelle und flexible Codierung.

var lookup = new Dictionary<int, dynamic>();
conn.Query<dynamic, dynamic, dynamic>(@"
    SELECT A.*, B.*
    FROM Client A
    INNER JOIN Instance B ON A.ClientID = B.ClientID                
    ", (A, B) => {
        // If dict has no key, allocate new obj
        // with another level of array
        if (!lookup.ContainsKey(A.ClientID)) {
            lookup[A.ClientID] = new {
                ClientID = A.ClientID,
                ClientName = A.Name,                                        
                Instances = new List<dynamic>()
            };
        }

        // Add each instance                                
        lookup[A.ClientID].Instances.Add(new {
            InstanceName = B.Name,
            BaseURL = B.BaseURL,
            WebAppPath = B.WebAppPath
        });

        return lookup[A.ClientID];
    }, splitOn: "ClientID,InstanceID").AsQueryable();

var resultList = lookup.Values;
return resultList;
Kiichi
quelle