Jak mapować listy zagnieżdżonych obiektów za pomocą Dapper

127

Obecnie używam Entity Framework do mojego dostępu do bazy danych, ale chcę rzucić okiem na Dapper. Mam takie zajęcia:

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

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

Tak więc jeden kurs może być prowadzony w kilku miejscach. Entity Framework wykonuje dla mnie mapowanie, więc mój obiekt kursu jest wypełniony listą lokalizacji. Jak mam to zrobić z Dapper, czy to w ogóle możliwe, czy też muszę to zrobić w kilku krokach zapytania?

b3n
źródło
Powiązane pytanie: stackoverflow.com/questions/6379155/…
Jeroen K
oto moje rozwiązanie: stackoverflow.com/a/57395072/8526957
Sam Sch

Odpowiedzi:

57

Dapper nie jest w pełni rozwiniętym ORMem, nie obsługuje magicznego generowania zapytań i tym podobnych.

W twoim konkretnym przykładzie prawdopodobnie zadziałałoby:

Weź udział w kursach:

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

Pobierz odpowiednie mapowanie:

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

Chwyć odpowiednie lokalizacje

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

Mapuj to wszystko

Pozostawiając to czytelnikowi, tworzysz kilka map i iterujesz po swoich kursach zapełniających lokalizacje.

Zastrzeżeniein trik zadziała, jeśli masz mniej niż 2100 wyszukiwań (SQL Server), jeśli masz więcej prawdopodobnie chcesz zmienić zapytanie, aby select * from CourseLocations where CourseId in (select Id from Courses ... )jeśli jest to sprawa może równie dobrze można szarpać wszystkich wyników za jednym razem za pomocąQueryMultiple

Sam Saffron
źródło
Dzięki za wyjaśnienie Sam. Tak jak opisałeś powyżej, po prostu uruchamiam drugie zapytanie, pobierając lokalizacje i ręcznie przypisując je do kursu. Chciałem się tylko upewnić, że nie przegapiłem czegoś, co pozwoliłoby mi to zrobić jednym zapytaniem.
b3n
2
Sam, w ~ dużej aplikacji, w której kolekcje są regularnie ujawniane w obiektach domeny, jak w przykładzie, gdzie zalecałbyś fizyczną lokalizację tego kodu ? (Zakładając, że chciałbyś wykorzystać podobnie w pełni skonstruowaną jednostkę [Kurs] z wielu różnych miejsc w kodzie) W konstruktorze? W klasowej fabryce? Gdzieś indziej?
tbone
177

Alternatywnie możesz użyć jednego zapytania z wyszukiwaniem:

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;

Zobacz tutaj https://www.tritac.com/blog/dappernet-by-example/

Jeroen K.
źródło
9
To zaoszczędziło mi mnóstwo czasu. Jedną modyfikacją, której potrzebowałem, a której inni mogą potrzebować, jest dołączenie argumentu splitOn:, ponieważ nie używałem domyślnego „Id”.
Bill Sambrone,
1
W przypadku LEFT JOIN otrzymasz zerową pozycję na liście lokalizacji. Usuń je przez var items = lookup.Values; items.ForEach (x => x.Locations.RemoveAll (y => y == null));
Choco Smith
Nie mogę tego skompilować, chyba że mam średnik na końcu wiersza 1 i usunę przecinek przed „AsQueryable ()”. Zmieniłbym odpowiedź, ale 62 pozytywnych głosów przede mną wydawało się, że wszystko jest w porządku, może czegoś mi brakuje ...
bitcoder
1
Dla LEFT JOIN: Nie musisz robić na nim kolejnego Foreach. Po prostu sprawdź przed dodaniem: if (l! = Null) course.Locations.Add (l).
jpgrassi
1
Ponieważ używasz słownika. Czy byłoby to szybsze, gdybyś użył QueryMultiple i wyszukałeś kurs i lokalizację oddzielnie, a następnie użyłeś tego samego słownika do przypisania lokalizacji do kursu? Zasadniczo jest to to samo, bez sprzężenia wewnętrznego, co oznacza, że ​​sql nie prześle tylu bajtów?
MIKE
43

Nie ma potrzeby używania lookupsłownika

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
źródło
3
To świetnie - to moim zdaniem powinna być wybrana odpowiedź. Jednak ludzie, którzy to robią, zwracają uwagę na robienie *, ponieważ może to wpłynąć na wydajność.
cr1pto
2
Jedynym problemem jest to, że będziesz kopiować nagłówek w każdym rekordzie lokalizacji. Jeśli istnieje wiele lokalizacji na kurs, może to być znaczna ilość duplikowanych danych przechodzących przez przewód, co zwiększy przepustowość, zajmie więcej czasu na analizę / mapowanie i zużyje więcej pamięci do odczytania tego wszystkiego.
Daniel Lorenz,
10
nie jestem pewien, czy to działa tak, jak się spodziewałem. Mam 1 obiekt nadrzędny z 3 powiązanymi obiektami. zapytanie, którego używam, zwraca trzy wiersze. pierwsze kolumny opisujące rodzica, które są zduplikowane dla każdego wiersza; podział na identyfikator zidentyfikowałby każde unikalne dziecko. moje wyniki to 3 zduplikowane rodzice z 3 dziećmi .... powinien być jednym rodzicem z 3 dziećmi.
topwik
2
@ topwik ma rację. dla mnie też nie działa zgodnie z oczekiwaniami.
Maciej Pszczoliński
3
Właściwie skończyło się na 3 rodzicach, po 1 dziecku w każdym z tym kodem. Nie jestem pewien, dlaczego mój wynik jest inny niż @topwik, ale nadal nie działa.
th3morg
29

Wiem, że jestem na to naprawdę spóźniony, ale jest inna opcja. Możesz użyć QueryMultiple tutaj. Coś takiego:

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
źródło
3
Jedna uwaga. Jeśli jest wiele lokalizacji / kursów, powinieneś raz przejrzeć lokalizacje i umieścić je w wyszukiwaniu w słowniku, aby uzyskać prędkość N log N zamiast N ^ 2. Robi dużą różnicę w większych zbiorach danych.
Daniel Lorenz,
6

Przepraszam, że spóźniłem się na imprezę (jak zawsze). Dla mnie łatwiej jest używać a Dictionary, tak jak zrobił to Jeroen K. , pod względem wydajności i czytelności. Ponadto, aby uniknąć mnożenia nagłówków w różnych lokalizacjach , używam Distinct()do usuwania potencjalnych duplikatów:

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
źródło
4

Coś brakuje. Jeśli nie określisz każdego pola Locationsw zapytaniu SQL, obiekt Locationnie może zostać wypełniony. Spójrz:

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;

Używając l.*w zapytaniu, miałem listę lokalizacji, ale bez danych.

Eduardo Pires
źródło
0

Nie jestem pewien, czy ktoś tego potrzebuje, ale mam dynamiczną wersję bez modelu do szybkiego i elastycznego kodowania.

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
źródło