¿Cómo mapeo listas de objetos anidados con Dapper

127

Actualmente estoy usando Entity Framework para mi acceso a base de datos, pero quiero echar un vistazo a Dapper. Tengo clases como esta:

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

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

Por lo tanto, un curso se puede enseñar en varios lugares. Entity Framework hace el mapeo por mí, por lo que mi objeto del curso se llena con una lista de ubicaciones. ¿Cómo haría esto con Dapper, es posible o tengo que hacerlo en varios pasos de consulta?

b3n
fuente
Pregunta relacionada: stackoverflow.com/questions/6379155/…
Jeroen K
Aquí está mi solución: stackoverflow.com/a/57395072/8526957
Sam Sch

Respuestas:

57

Dapper no es un ORM completo, no maneja la generación mágica de consultas y demás.

Para su ejemplo particular, lo siguiente probablemente funcionaría:

Agarra los cursos:

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

Agarra el mapeo relevante:

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

Agarra las ubicaciones relevantes

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

Mapearlo todo

Dejando esto al lector, crea algunos mapas e itera a través de los cursos que se completan con las ubicaciones.

Tenga en cuenta que el intruco funcionará si tiene menos de 2100 búsquedas (Sql Server), si tiene más, probablemente desee modificar la consulta, select * from CourseLocations where CourseId in (select Id from Courses ... )si ese es el caso, también puede eliminar todos los resultados de una vez usandoQueryMultiple

Sam Azafrán
fuente
Gracias por la aclaración Sam. Como describió anteriormente, solo estoy ejecutando una segunda consulta para buscar las ubicaciones y asignarlas manualmente al curso. Solo quería asegurarme de que no me perdiera algo que me permitiera hacerlo con una sola consulta.
b3n
2
Sam, en una aplicación ~ grande donde las colecciones se exponen regularmente en objetos de dominio como en el ejemplo, ¿ dónde recomendarías que este código esté ubicado físicamente ? (Suponiendo que desea consumir una entidad [Curso] completamente construida de manera similar desde numerosos lugares diferentes en su código) ¿En el constructor? En una fábrica de clase? ¿En algún otro lugar?
tbone
177

Alternativamente, puede usar una consulta con una búsqueda:

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;

Ver aquí https://www.tritac.com/blog/dappernet-by-example/

Jeroen K
fuente
9
Esto me ahorró un montón de tiempo. Una modificación que necesitaba que otros puedan necesitar es incluir el argumento splitOn: ya que no estaba usando el "Id" predeterminado.
Bill Sambrone
1
Para IZQUIERDA UNIRSE, obtendrá un elemento nulo en la lista de ubicaciones. Eliminarlos por var items = lookup.Values; items.ForEach (x => x.Locations.RemoveAll (y => y == null));
Choco Smith
No puedo compilar esto a menos que tenga un punto y coma al final de la línea 1 y elimine la coma antes de 'AsQueryable ()'. Me gustaría editar la respuesta, pero 62 upvoters antes me parecía que creo que está bien, tal vez me falta algo ...
bitcoder
1
Para IZQUIERDA UNIRSE: No necesita hacer otro Foreach en él. Simplemente verifique antes de agregarlo: if (l! = Null) course.Locations.Add (l).
jpgrassi
1
Ya que estás usando un diccionario. ¿Sería esto más rápido si usara QueryMultiple y consultara el curso y la ubicación por separado y luego usara el mismo diccionario para asignar la ubicación al curso? Es esencialmente lo mismo menos la unión interna, lo que significa que sql no transferirá tantos bytes.
MIKE
43

No hay necesidad de lookupdiccionario

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
fuente
3
Esto es excelente, en mi opinión, esta debería ser la respuesta seleccionada. Sin embargo, las personas que hacen esto, tengan cuidado con hacer * ya que puede afectar el rendimiento.
cr1pto
2
El único problema con esto es que duplicará el encabezado en cada registro de ubicación. Si hay muchas ubicaciones por curso, podría ser una cantidad significativa de duplicación de datos a través del cable lo que aumentará el ancho de banda, tomará más tiempo analizar / asignar y usar más memoria para leer todo eso.
Daniel Lorenz
10
No estoy seguro de que esto funcione como esperaba. Tengo 1 objeto padre con 3 objetos relacionados. la consulta que uso recupera tres filas. las primeras columnas que describen el padre que se duplican para cada fila; la división en la identificación identificaría a cada hijo único. mis resultados son 3 padres duplicados con 3 hijos ... debería ser un padre con 3 hijos.
topwik
2
@topwik tiene razón. tampoco funciona como se esperaba para mí.
Maciej Pszczolinski
3
En realidad terminé con 3 padres, 1 niño en cada uno con este código. No estoy seguro de por qué mi resultado es diferente a @topwik, pero aún así, no funciona.
th3morg
29

Sé que realmente llego tarde a esto, pero hay otra opción. Puedes usar QueryMultiple aquí. Algo como esto:

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
fuente
3
Una cosa a tener en cuenta. Si hay muchas ubicaciones / cursos, debe recorrer las ubicaciones una vez y ponerlas en una búsqueda de diccionario para que tenga N log N en lugar de N ^ 2 velocidad. Hace una gran diferencia en conjuntos de datos más grandes.
Daniel Lorenz
6

Perdón por llegar tarde a la fiesta (como siempre). Para mí, es más fácil usar un Dictionary, como lo hizo Jeroen K , en términos de rendimiento y legibilidad. Además, para evitar la multiplicación de encabezados entre ubicaciones , utilizo Distinct()para eliminar duplicaciones potenciales:

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
fuente
4

Algo falta. Si no especifica cada campo Locationsen la consulta SQL, el objeto Locationno se puede completar. Echar un vistazo:

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.*en la consulta, tenía la lista de ubicaciones pero sin datos.

Eduardo Pires
fuente
0

No estoy seguro de si alguien lo necesita, pero tengo una versión dinámica sin Modelo para una codificación rápida y flexible.

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
fuente