161 votes

Comment mettre en correspondance des listes d'objets imbriqués avec Dapper ?

J'utilise actuellement Entity Framework pour l'accès à ma base de données, mais je voudrais jeter un coup d'œil à Dapper. J'ai des classes comme celle-ci :

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

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

Un même cours peut donc être dispensé en plusieurs endroits. Entity Framework fait le mappage pour moi, de sorte que mon objet Cours est rempli d'une liste de lieux. Comment puis-je faire cela avec Dapper, est-ce possible ou dois-je le faire en plusieurs étapes de requête ?

2 votes

1 votes

215voto

Jeroen K Points 1647

Vous pouvez également utiliser une requête avec une recherche :

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;

Voir ici https://www.tritac.com/blog/dappernet-by-example/

15 votes

Cela m'a fait gagner beaucoup de temps. Une modification dont j'ai eu besoin et dont d'autres peuvent avoir besoin est d'inclure l'argument splitOn : puisque je n'utilisais pas le "Id" par défaut.

2 votes

Pour le LEFT JOIN, vous obtiendrez un élément nul dans la liste des emplacements. Supprimez-les en var items = lookup.Values ; items.ForEach(x => x.Locations.RemoveAll(y => y == null)) ;

1 votes

Je ne peux pas compiler ce fichier à moins de mettre un point-virgule à la fin de la ligne 1 et d'enlever la virgule avant 'AsQueryable()'. Je modifierais bien la réponse mais 62 votants avant moi semblent penser que c'est correct, peut-être que quelque chose m'échappe...

64voto

Sam Saffron Points 56236

Dapper n'est pas un ORM complet, il ne gère pas la génération magique de requêtes et autres.

Pour votre exemple particulier, la solution suivante fonctionnerait probablement :

Prenez les cours :

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

Prenez la cartographie correspondante :

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

Saisissez les emplacements pertinents

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

Cartographier le tout

En laissant le soin au lecteur de s'en occuper, vous créez quelques cartes et vous parcourez vos cours en y ajoutant les lieux.

Caveat el in L'astuce fonctionne si vous avez moins de 2100 (Sql Server), si vous en avez plus, vous voudrez probablement modifier la requête pour select * from CourseLocations where CourseId in (select Id from Courses ... ) si c'est le cas, vous pouvez aussi bien arracher tous les résultats en une seule fois en utilisant QueryMultiple

1 votes

Merci pour cette clarification, Sam. Comme vous l'avez décrit ci-dessus, j'exécute simplement une deuxième requête qui récupère les lieux et les affecte manuellement au cours. Je voulais juste m'assurer que je n'avais pas oublié quelque chose qui me permettrait de le faire avec une seule requête.

3 votes

Sam, dans une ~grande application où les collections sont régulièrement exposées sur les objets du domaine comme dans l'exemple, où recommandez-vous que ce code soit physiquement situé ? (En supposant que vous souhaitiez consommer une entité [Cours] entièrement construite de la même manière, à partir de nombreux différents endroits dans votre code) Dans le constructeur ? Dans une fabrique de classe ? Quelque part ailleurs ?

47voto

tchelidze Points 4891

Pas besoin de lookup Dictionnaire

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

4 votes

C'est excellent - à mon avis, ce devrait être la réponse choisie. Les personnes qui font cela doivent cependant faire attention à ne pas faire *, car cela peut avoir un impact sur les performances.

1 votes

@randomus1r, je ne suis pas sûr de ce que vous voulez dire. Ma remarque met en garde les utilisateurs contre l'utilisation de SELECT * en général, et ne dit pas qu'il n'utilise pas SELECT * correctement.

6 votes

Le seul problème est que vous allez dupliquer l'en-tête sur chaque enregistrement de lieu. S'il y a beaucoup de lieux par cours, cela pourrait représenter une quantité importante de données dupliquées sur le fil, ce qui augmenterait la bande passante, prendrait plus de temps à analyser/mapper et utiliserait plus de mémoire pour lire tout cela.

40voto

Daniel Lorenz Points 429

Je sais que je suis vraiment en retard sur ce sujet, mais il y a une autre option. Vous pouvez utiliser QueryMultiple ici. Quelque chose comme ça :

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

9 votes

Une chose à noter. S'il y a beaucoup de lieux/cours, vous devriez faire une boucle à travers les lieux une fois et les mettre dans un dictionnaire de recherche afin d'avoir N log N au lieu de N^2 vitesse. Cela fait une grande différence dans les grands ensembles de données.

10voto

Francisco Tena Points 124

Désolé d'être en retard à la fête (comme toujours). Pour moi, il est plus facile d'utiliser une Dictionary , comme l'a fait Jeroen K en termes de performances et de lisibilité. De plus, pour éviter la multiplication des en-têtes à travers sites J'utilise Distinct() pour supprimer les doublons potentiels :

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X