Come faccio a mappare elenchi di oggetti nidificati con Dapper


127

Attualmente sto usando Entity Framework per il mio accesso al database, ma voglio dare un'occhiata a Dapper. Ho classi come questa:

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

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

Quindi un corso può essere tenuto in diversi luoghi. Entity Framework esegue il mapping per me, quindi il mio oggetto Course viene popolato con un elenco di posizioni. Come dovrei farlo con Dapper, è anche possibile o devo farlo in diversi passaggi di query?



Risposte:


57

Dapper non è un ORM in piena regola, non gestisce la generazione magica di query e simili.

Per il tuo esempio particolare, probabilmente funzionerebbe quanto segue:

Prendi i corsi:

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

Prendi la mappatura pertinente:

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

Prendi le posizioni pertinenti

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

Mappa tutto

Lasciando questo al lettore, crei alcune mappe e ripeti i tuoi corsi popolando le posizioni.

Attenzione, il intrucco funzionerà se hai meno di 2100 ricerche (Sql Server), se ne hai di più probabilmente vorrai modificare la query select * from CourseLocations where CourseId in (select Id from Courses ... )se questo è il caso puoi anche estrarre tutti i risultati in una volta usandoQueryMultiple


Grazie per il chiarimento Sam. Come hai descritto sopra, sto solo eseguendo una seconda query recuperando le posizioni e assegnandole manualmente al corso. Volevo solo assicurarmi di non perdere qualcosa che mi avrebbe permesso di farlo con una sola query.
b3n

2
Sam, in un'applicazione di grandi dimensioni in cui le raccolte sono regolarmente esposte su oggetti di dominio come nell'esempio, dove consiglieresti di collocare fisicamente questo codice ? (Supponendo che tu voglia utilizzare un'entità [Corso] costruita in modo simile da numerosi punti diversi nel tuo codice) Nel costruttore? In una fabbrica di classe? Altrove?
tbone

178

In alternativa, puoi utilizzare una query con una ricerca:

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;

Vedi qui https://www.tritac.com/blog/dappernet-by-example/


9
Questo mi ha fatto risparmiare un sacco di tempo. Una modifica di cui avevo bisogno e che altri potrebbero aver bisogno è includere l'argomento splitOn: poiché non stavo usando l '"Id" predefinito.
Bill Sambrone

1
Per LEFT JOIN otterrai un elemento nullo nell'elenco delle posizioni. Rimuovili con var items = lookup.Values; items.ForEach (x => x.Locations.RemoveAll (y => y == null));
Choco Smith

Non posso compilarlo a meno che non abbia un punto e virgola alla fine della riga 1 e rimuova la virgola prima di 'AsQueryable ()'. Vorrei modificare la risposta ma 62 votanti prima di me sembravano pensare che fosse ok, forse mi manca qualcosa ...
bitcoder

1
Per LEFT JOIN: non c'è bisogno di fare un altro Foreach su di esso. Controlla prima di aggiungerlo: if (l! = Null) course.Locations.Add (l).
jpgrassi

1
Dato che stai usando un dizionario. Sarebbe più veloce se usassi QueryMultiple e interrogassi il corso e la posizione separatamente, quindi utilizzassi lo stesso dizionario per assegnare la posizione al corso? È essenzialmente la stessa cosa meno l'unione interna, il che significa che sql non trasferirà tanti byte?
MIKE

43

Non c'è bisogno del lookupdizionario

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

3
Questo è eccellente - questa secondo me dovrebbe essere la risposta scelta. Le persone che lo fanno, però, fanno attenzione a farlo * poiché ciò può influire sulle prestazioni.
cr1pto

2
L'unico problema con questo è che duplicherai l'intestazione su ogni record di posizione. Se ci sono molte posizioni per corso, potrebbe essere una quantità significativa di duplicazione dei dati che attraversa il cavo che aumenterà la larghezza di banda, impiegherà più tempo per analizzare / mappare e utilizzare più memoria per leggere tutto ciò.
Daniel Lorenz

10
Non sono sicuro che funzioni come mi aspettavo. ho 1 oggetto genitore con 3 oggetti correlati. la query che utilizzo torna indietro di tre righe. le prime colonne che descrivono il genitore che sono duplicate per ogni riga; la divisione su id identifica ogni figlio unico. i miei risultati sono 3 genitori duplicati con 3 figli .... dovrebbe essere un genitore con 3 figli.
topwik

2
@topwik ha ragione. non funziona come previsto neanche per me.
Maciej Pszczolinski

3
In realtà sono finito con 3 genitori, 1 bambino in ciascuno con questo codice. Non sono sicuro del motivo per cui il mio risultato è diverso da @topwik, ma comunque non funziona.
th3morg

29

So di essere davvero in ritardo, ma c'è un'altra opzione. Puoi usare QueryMultiple qui. Qualcosa come questo:

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

3
Una cosa da notare. Se ci sono molti luoghi / percorsi, dovresti scorrere i luoghi una volta e metterli in una ricerca nel dizionario in modo da avere N log N invece di N ^ 2 velocità. Fa una grande differenza in set di dati più grandi.
Daniel Lorenz

6

Scusa il ritardo alla festa (come sempre). Per me, è più facile usare un Dictionary, come ha fatto Jeroen K , in termini di prestazioni e leggibilità. Inoltre, per evitare la moltiplicazione delle intestazioni tra le posizioni , utilizzo Distinct()per rimuovere potenziali duplicati:

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

4

Manca qualcosa. Se non si specifica ogni campo da Locationsnella query SQL, l'oggetto Locationnon può essere riempito. Guarda:

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;

Usando l.*nella query, avevo l'elenco delle posizioni ma senza dati.


0

Non sono sicuro che qualcuno ne abbia bisogno, ma ne ho una versione dinamica senza Modello per una codifica rapida e flessibile.

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