Multi-Mapper pour créer une hiérarchie d'objets

dapper multi-mapping

Question

J'ai joué un peu avec cela, car il semble que cela ressemble beaucoup à l' exemple de messages / utilisateurs documentés , mais il est légèrement différent et ne fonctionne pas pour moi.

En supposant la configuration simplifiée suivante (un contact a plusieurs numéros de téléphone):

public class Contact
{
    public int ContactID { get; set; }
    public string ContactName { get; set; }
    public IEnumerable<Phone> Phones { get; set; }
}

public class Phone
{
    public int PhoneId { get; set; }
    public int ContactID { get; set; } // foreign key
    public string Number { get; set; }
    public string Type { get; set; }
    public bool IsActive { get; set; }
}

Je voudrais bien finir avec quelque chose qui retourne un contact avec plusieurs objets de téléphone. De cette façon, si j'avais 2 contacts, avec 2 téléphones chacun, mon SQL renverrait une jointure de ceux-ci avec un jeu de 4 lignes au total. Ensuite, Dapper afficherait 2 objets de contact avec deux téléphones chacun.

Voici le SQL dans la procédure stockée:

SELECT *
FROM Contacts
    LEFT OUTER JOIN Phones ON Phones.ReferenceId=Contacts.ReferenceId
WHERE clientid=1

J'ai essayé ceci, mais j'ai fini avec 4 tuples (ce qui est correct, mais pas ce que j'espérais ... cela signifie simplement que je dois encore normaliser le résultat):

var x = cn.Query<Contact, Phone, Tuple<Contact, Phone>>("sproc_Contacts_SelectByClient",
                              (co, ph) => Tuple.Create(co, ph), 
                                          splitOn: "PhoneId", param: p, 
                                          commandType: CommandType.StoredProcedure);

et quand j'essaie une autre méthode (ci-dessous), j'obtiens une exception "Impossible de convertir l'objet de type 'System.Int32' pour taper 'System.Collections.Generic.IEnumerable`1 [Phone]'."

var x = cn.Query<Contact, IEnumerable<Phone>, Contact>("sproc_Contacts_SelectByClient",
                               (co, ph) => { co.Phones = ph; return co; }, 
                                             splitOn: "PhoneId", param: p,
                                             commandType: CommandType.StoredProcedure);

Est-ce que je fais juste quelque chose de mal? Cela ressemble à l'exemple des messages / propriétaire, sauf que je vais du parent à l'enfant au lieu de l'enfant au parent.

Merci d'avance

Réponse acceptée

Vous ne faites rien de mal, ce n'est pas la façon dont l'API a été conçue. Toutes les API de Query renverront toujours un objet par ligne de base de données.

Donc, cela fonctionne bien sur les nombreuses -> une directions, mais moins bien pour celui -> beaucoup de multi-cartes.

Il y a 2 problèmes ici:

  1. Si nous introduisons un mappeur intégré qui fonctionne avec votre requête, nous sommes supposés "ignorer" les données en double. (Contacts. * Est dupliqué dans votre requête)

  2. Si nous le concevons pour fonctionner avec une paire unique, nous aurons besoin d'une sorte de carte d'identité. Ce qui ajoute de la complexité.


Prenons par exemple cette requête qui est efficace si vous avez juste besoin d'extraire un nombre limité d'enregistrements, si vous poussez ce nombre jusqu'à un million d'objets, devenez plus compliqué, car vous devez diffuser et ne pouvez pas tout charger en mémoire:

var sql = "set nocount on
DECLARE @t TABLE(ContactID int,  ContactName nvarchar(100))
INSERT @t
SELECT *
FROM Contacts
WHERE clientid=1
set nocount off 
SELECT * FROM @t 
SELECT * FROM Phone where ContactId in (select t.ContactId from @t t)"

Ce que vous pouvez faire est d'étendre le GridReader pour permettre le remappage:

var mapped = cnn.QueryMultiple(sql)
   .Map<Contact,Phone, int>
    (
       contact => contact.ContactID, 
       phone => phone.ContactID,
       (contact, phones) => { contact.Phones = phones };  
    );

En supposant que vous étendez votre GridReader et avec un mappeur:

public static IEnumerable<TFirst> Map<TFirst, TSecond, TKey>
    (
    this GridReader reader,
    Func<TFirst, TKey> firstKey, 
    Func<TSecond, TKey> secondKey, 
    Action<TFirst, IEnumerable<TSecond>> addChildren
    )
{
    var first = reader.Read<TFirst>().ToList();
    var childMap = reader
        .Read<TSecond>()
        .GroupBy(s => secondKey(s))
        .ToDictionary(g => g.Key, g => g.AsEnumerable());

    foreach (var item in first)
    {
        IEnumerable<TSecond> children;
        if(childMap.TryGetValue(firstKey(item), out children))
        {
            addChildren(item,children);
        }
    }

    return first;
}

Puisque c'est un peu difficile et complexe, avec des mises en garde. Je ne penche pas pour inclure cela dans le noyau.


Réponse populaire

FYI - J'ai obtenu la réponse de Sam en faisant ce qui suit:

Tout d'abord, j'ai ajouté un fichier de classe appelé "Extensions.cs". J'ai dû remplacer le mot-clé "this" par "reader" à deux endroits:

using System;
using System.Collections.Generic;
using System.Linq;
using Dapper;

namespace TestMySQL.Helpers
{
    public static class Extensions
    {
        public static IEnumerable<TFirst> Map<TFirst, TSecond, TKey>
            (
            this Dapper.SqlMapper.GridReader reader,
            Func<TFirst, TKey> firstKey,
            Func<TSecond, TKey> secondKey,
            Action<TFirst, IEnumerable<TSecond>> addChildren
            )
        {
            var first = reader.Read<TFirst>().ToList();
            var childMap = reader
                .Read<TSecond>()
                .GroupBy(s => secondKey(s))
                .ToDictionary(g => g.Key, g => g.AsEnumerable());

            foreach (var item in first)
            {
                IEnumerable<TSecond> children;
                if (childMap.TryGetValue(firstKey(item), out children))
                {
                    addChildren(item, children);
                }
            }

            return first;
        }
    }
}

Deuxièmement, j'ai ajouté la méthode suivante, en modifiant le dernier paramètre:

public IEnumerable<Contact> GetContactsAndPhoneNumbers()
{
    var sql = @"
SELECT * FROM Contacts WHERE clientid=1
SELECT * FROM Phone where ContactId in (select ContactId FROM Contacts WHERE clientid=1)";

    using (var connection = GetOpenConnection())
    {
        var mapped = connection.QueryMultiple(sql)    
            .Map<Contact,Phone, int>     (        
            contact => contact.ContactID,        
            phone => phone.ContactID,
            (contact, phones) => { contact.Phones = phones; }      
        ); 
        return mapped;
    }
}



Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Est-ce KB légal? Oui, apprenez pourquoi
Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Est-ce KB légal? Oui, apprenez pourquoi