Multi-Mapper para crear jerarquía de objetos

dapper multi-mapping

Pregunta

He estado jugando con esto un poco, porque parece que se parece mucho al ejemplo documentado de posts / users , pero es un poco diferente y no funciona para mí.

Asumiendo la siguiente configuración simplificada (un contacto tiene múltiples números de teléfono):

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

Me encantaría terminar con algo que devuelva un contacto con múltiples objetos de teléfono. De esa manera, si tuviera 2 contactos, con 2 teléfonos cada uno, mi SQL devolvería una combinación de ellos como resultado conjunto con 4 filas totales. Entonces Dapper sacaría dos objetos de contacto con dos teléfonos cada uno.

Aquí está el SQL en el procedimiento almacenado:

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

Intenté esto, pero terminé con 4 Tuples (lo cual está bien, pero no es lo que esperaba ... solo significa que todavía tengo que volver a normalizar el resultado):

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

y cuando pruebo otro método (a continuación), obtengo una excepción de "No se puede lanzar el objeto de tipo 'System.Int32' para escribir '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);

¿Estoy haciendo algo mal? Parece justo como el ejemplo de posts / owner, excepto que voy del padre al hijo en lugar del hijo al padre.

Gracias por adelantado

Respuesta aceptada

No estás haciendo nada mal, simplemente no es la forma en que se diseñó la API. Todas las API de Query siempre devolverán un objeto por fila de base de datos.

Por lo tanto, esto funciona bien en muchos -> una dirección, pero menos para uno -> muchos multi-mapas.

Hay 2 problemas aquí:

  1. Si presentamos un mapeador incorporado que funcione con su consulta, se esperaría que "descartemos" los datos duplicados. (Contactos. * Está duplicado en su consulta)

  2. Si lo diseñamos para que funcione con un par de uno o varios, necesitaremos algún tipo de mapa de identidad. Lo cual agrega complejidad.


Tomemos, por ejemplo, esta consulta que es eficiente si solo necesita extraer un número limitado de registros, si empuja esto hasta un millón de cosas se vuelve más complicado, porque necesita transmitir y no puede cargar todo en la memoria:

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)"

Lo que podría hacer es extender el GridReader para permitir la reasignación:

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

Suponiendo que extiende su GridReader y con un asignador:

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

Dado que esto es un poco complicado y complejo, con advertencias. No estoy inclinado a incluir esto en el núcleo.


Respuesta popular

FYI - Obtuve la respuesta de Sam trabajando haciendo lo siguiente:

Primero, agregué un archivo de clase llamado "Extensions.cs". Tuve que cambiar la palabra clave "this" por "reader" en dos lugares:

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

Segundo, agregué el siguiente método, modificando el último parámetro:

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


Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow
¿Es esto KB legal? Sí, aprende por qué
Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow
¿Es esto KB legal? Sí, aprende por qué