Comment mapper des listes d'objets imbriqués avec Dapper


127

J'utilise actuellement Entity Framework pour mon accès à la base de données, mais je souhaite jeter un coup d'œil à Dapper. J'ai des cours comme celui-ci:

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

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

Un cours peut donc être enseigné à plusieurs endroits. Entity Framework fait le mappage pour moi afin que mon objet Course soit rempli avec une liste d'emplacements. Comment procéder avec Dapper, est-ce possible ou dois-je le faire en plusieurs étapes de requête?



Réponses:


57

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

Pour votre exemple particulier, ce qui suit fonctionnerait probablement:

Prenez les cours:

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

Prenez la cartographie appropriée:

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

Cartographiez tout

En laissant cela au lecteur, vous créez quelques cartes et parcourez vos parcours en remplissant les emplacements.

Attention, l' inastuce fonctionnera si vous avez moins de 2100 recherches (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 tel est le cas, vous pouvez également extraire tous les résultats en une seule fois en utilisantQueryMultiple


Merci pour la clarification Sam. Comme vous l'avez décrit ci-dessus, j'exécute simplement une deuxième requête pour récupérer les emplacements et les attribuer manuellement au cours. Je voulais juste m'assurer de ne pas manquer quelque chose qui me permettrait de le faire avec une seule requête.
b3n

2
Sam, dans une ~ grande application où les collections sont régulièrement exposées sur des objets de domaine comme dans l'exemple, où recommanderiez-vous que ce code soit physiquement localisé ? (En supposant que vous souhaitiez consommer une entité [Course] entièrement construite de manière similaire à de nombreux endroits différents dans votre code) Dans le constructeur? Dans une usine de classe? Ailleurs?
tbone

178

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/


9
Cela m'a fait gagner beaucoup de temps. Une modification dont j'avais besoin et dont d'autres auraient besoin est d'inclure l'argument splitOn: puisque je n'utilisais pas le "Id" par défaut.
Bill Sambrone

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

Je ne peux pas compiler cela à moins d'avoir un point-virgule à la fin de la ligne 1 et de supprimer la virgule avant le 'AsQueryable ()'. Je modifierais la réponse mais 62 votants positifs avant moi semblaient penser que c'était correct, peut-être que je manque quelque chose ...
bitcoder

1
Pour LEFT JOIN: Pas besoin de faire un autre Foreach dessus. Il suffit de vérifier avant de l'ajouter: if (l! = Null) course.Locations.Add (l).
jpgrassi

1
Puisque vous utilisez un dictionnaire. Serait-ce plus rapide si vous utilisiez QueryMultiple et le cours et l'emplacement interrogés séparément, puis utilisiez le même dictionnaire pour attribuer un emplacement au cours? C'est essentiellement la même chose moins la jointure interne, ce qui signifie que sql ne transférera pas autant d'octets?
MIKE

43

Pas besoin de lookupdictionnaire

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
C'est excellent - cela devrait à mon avis être la réponse choisie. Les gens qui font cela, cependant, faites attention à le faire * car cela peut avoir un impact sur les performances.
cr1pto

2
Le seul problème avec cela est que vous dupliquerez l'en-tête sur chaque enregistrement d'emplacement. S'il y a beaucoup d'emplacements par cours, cela pourrait être une quantité importante de duplication de données sur le fil qui augmentera la bande passante, prendra plus de temps à analyser / mapper et utiliser plus de mémoire pour lire tout cela.
Daniel Lorenz

10
Je ne suis pas sûr que cela fonctionne comme prévu. J'ai 1 objet parent avec 3 objets associés. la requête que j'utilise récupère trois lignes. les premières colonnes décrivant le parent qui sont dupliquées pour chaque ligne; le partage sur id identifierait chaque enfant unique. mes résultats sont 3 parents en double avec 3 enfants .... devrait être un parent avec 3 enfants.
topwik

2
@topwik a raison. cela ne fonctionne pas non plus comme prévu pour moi.
Maciej Pszczolinski

3
En fait, je me suis retrouvé avec 3 parents, 1 enfant dans chacun avec ce code. Je ne sais pas pourquoi mon résultat est différent de @topwik, mais cela ne fonctionne toujours pas.
th3morg

29

Je sais que je suis vraiment en retard, 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();
}

3
Une chose à noter. S'il y a beaucoup d'emplacements / cours, vous devez parcourir les emplacements une fois et les placer dans une recherche de dictionnaire afin d'avoir N log N au lieu de N ^ 2 vitesse. Fait une grande différence dans les ensembles de données plus volumineux.
Daniel Lorenz

6

Désolé d'être en retard à la fête (comme toujours). Pour moi, il est plus facile d'utiliser un Dictionary, comme l'a fait Jeroen K , en termes de performances et de lisibilité. De plus, pour éviter la multiplication des en-têtes entre les emplacements , 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;
}

4

Quelque chose manque. Si vous ne spécifiez pas chaque champ Locationsdans la requête SQL, l'objet Locationne peut pas être rempli. Regarde:

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;

En utilisant l.*dans la requête, j'avais la liste des emplacements mais sans données.


0

Je ne sais pas si quelqu'un en a besoin, mais j'en ai une version dynamique sans modèle pour un codage rapide et 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;
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.