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?

b3n
la source
Question connexe: stackoverflow.com/questions/6379155/…
Jeroen K
voici ma solution: stackoverflow.com/a/57395072/8526957
Sam Sch

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

Sam Safran
la source
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/

Jeroen K
la source
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();
tchelidze
la source
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();
}
Daniel Lorenz
la source
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;
}
Francisco Tena
la source
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.

Eduardo Pires
la source
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;
Kiichi
la source