84 votes

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

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

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

J'aimerais obtenir quelque chose qui renvoie un contact avec plusieurs objets téléphone. De cette façon, si j'ai 2 contacts, avec 2 téléphones chacun, mon SQL renverrait une jointure de ceux-ci comme un ensemble de résultats avec 4 lignes au total. Dapper renverrait alors deux objets 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é, mais je me suis retrouvé avec 4 Tuples (ce qui est correct, mais pas ce que j'espérais... cela signifie simplement que je dois encore re-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 lorsque j'essaie une autre méthode (ci-dessous), j'obtiens une exception de "Unable to cast object of type 'System.Int32' to type '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 quelque chose de mal ? Cela ressemble à l'exemple de posts/owner, sauf que je vais du parent vers l'enfant au lieu de l'enfant vers le parent.

Merci d'avance

70voto

Sam Saffron Points 56236

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

Ainsi, cela fonctionne bien dans le sens plusieurs -> un, mais moins bien pour la carte multiple un -> plusieurs.

Il y a deux problèmes ici :

  1. Si nous introduisons un mappeur intégré qui fonctionne avec votre requête, nous devrions "écarter" les données dupliquées. (Contacts.* est dupliqué dans votre requête)

  2. Si nous le concevons pour qu'il fonctionne avec une paire un -> plusieurs, nous aurons besoin d'une sorte de carte d'identité. Ce qui ajoute de la complexité.


Prenons l'exemple de cette requête qui est efficace si vous n'avez besoin que d'un nombre limité d'enregistrements. Si vous poussez cette requête jusqu'à un million, les choses deviennent plus délicates, car vous avez besoin de streamer 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 pourriez faire, c'est é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;
}

Comme c'est un peu délicat et complexe, avec des mises en garde. Je ne suis pas enclin à l'inclure dans le noyau.

0 votes

Très cool. Ce truc a une sacrée puissance... Je suppose qu'il faut juste s'habituer à l'utiliser. Je vais examiner la charge utile de mes requêtes et voir quelle est la taille des ensembles de résultats et voir si nous pouvons nous permettre d'avoir plusieurs requêtes et de les mapper ensemble.

0 votes

@Jorin, votre autre option serait d'orchestrer plusieurs connexions et de tisser les résultats. C'est un peu plus délicat.

0 votes

@Sam, la dernière ligne de remappage lue est : contact, phones => { contact.Phones = phones } ; ... au lieu de cela, cela devrait être : (contact, téléphones) => { contact.Téléphones = téléphones ; } ) ;

33voto

Mike Gleason Points 849

Pour info, j'ai réussi à faire fonctionner 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û changer le mot clé "this" en "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;
        }
    }
}

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

26voto

Jeroen K Points 1647

Vérifiez https://www.tritac.com/blog/dappernet-by-example/ Vous pourriez faire quelque chose comme ça :

public class Shop {
  public int? Id {get;set;}
  public string Name {get;set;}
  public string Url {get;set;}
  public IList<Account> Accounts {get;set;}
}

public class Account {
  public int? Id {get;set;}
  public string Name {get;set;}
  public string Address {get;set;}
  public string Country {get;set;}
  public int ShopId {get;set;}
}

var lookup = new Dictionary<int, Shop>()
conn.Query<Shop, Account, Shop>(@"
                  SELECT s.*, a.*
                  FROM Shop s
                  INNER JOIN Account a ON s.ShopId = a.ShopId                    
                  ", (s, a) => {
                       Shop shop;
                       if (!lookup.TryGetValue(s.Id, out shop)) {
                           lookup.Add(s.Id, shop = s);
                       }
                       shop.Accounts.Add(a);
                       return shop;
                   },
                   ).AsQueryable();
var resultList = lookup.Values;

J'ai trouvé ça dans les tests de dapper.net : https://code.google.com/p/dapper-dot-net/source/browse/Tests/Tests.cs#1343

2 votes

Wow ! Pour moi, c'est la solution la plus simple. Je vous l'accorde, pour un one->many (en supposant deux tables), j'opterais pour les doubles sélections. Cependant, dans mon cas, j'ai un->un->nombre et cela fonctionne très bien. Maintenant, cela ramène beaucoup de données redondantes mais dans mon cas, cette redondance est relativement faible - 10 lignes au mieux.

0 votes

Cela fonctionne bien pour deux niveaux, mais cela devient délicat lorsque vous en avez plus.

1 votes

S'il n'y a pas de données enfant, le code (s, a) sera appelé avec a=null, et Comptes contiendra une liste avec une entrée null au lieu d'être vide. Vous devez ajouter "if (a != null)" avant "shop.Accounts.Add(a)".

12voto

Robert Koritnik Points 45499

Support de plusieurs ensembles de résultats

Dans votre cas, il serait bien mieux (et plus facile aussi) d'avoir une requête multirésultats. Cela signifie simplement que vous devez écrire deux instructions select :

  1. Un qui renvoie des contacts
  2. Et une qui renvoie leurs numéros de téléphone

De cette façon, vos objets seront uniques et ne seront pas dupliqués.

1 votes

Bien que les autres réponses puissent être élégantes à leur manière, j'ai tendance à préférer celle-ci car le code est plus facile à raisonner. Je peux construire une hiérarchie de quelques niveaux avec une poignée d'instructions select et environ 30 lignes de code foreach/linq. Cela pourrait s'effondrer avec des ensembles de résultats massifs, mais heureusement je n'ai pas ce problème (encore).

11voto

Clay Points 1060

Voici une solution réutilisable qui est assez facile à utiliser. Il s'agit d'une légère modification de Réponse d'Andrews .

public static IEnumerable<TParent> QueryParentChild<TParent, TChild, TParentKey>(
    this IDbConnection connection,
    string sql,
    Func<TParent, TParentKey> parentKeySelector,
    Func<TParent, IList<TChild>> childSelector,
    dynamic param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null)
{
    Dictionary<TParentKey, TParent> cache = new Dictionary<TParentKey, TParent>();

    connection.Query<TParent, TChild, TParent>(
        sql,
        (parent, child) =>
            {
                if (!cache.ContainsKey(parentKeySelector(parent)))
                {
                    cache.Add(parentKeySelector(parent), parent);
                }

                TParent cachedParent = cache[parentKeySelector(parent)];
                IList<TChild> children = childSelector(cachedParent);
                children.Add(child);
                return cachedParent;
            },
        param as object, transaction, buffered, splitOn, commandTimeout, commandType);

    return cache.Values;
}

Exemple d'utilisation

public class Contact
{
    public int ContactID { get; set; }
    public string ContactName { get; set; }
    public List<Phone> Phones { get; set; } // must be IList

    public Contact()
    {
        this.Phones = new List<Phone>(); // POCO is responsible for instantiating child list
    }
}

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

conn.QueryParentChild<Contact, Phone, int>(
    "SELECT * FROM Contact LEFT OUTER JOIN Phone ON Contact.ContactID = Phone.ContactID",
    contact => contact.ContactID,
    contact => contact.Phones,
    splitOn: "PhoneId");

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