Multi-Mapper para crear jerarquías de objetos

82

He estado jugando con esto por un tiempo, porque parece que se parece mucho al ejemplo de publicaciones / usuarios documentados , pero es un poco diferente y no funciona para mí.

Suponiendo la siguiente configuración simplificada (un contacto tiene varios 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 varios 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 esos como un conjunto de resultados con 4 filas en total. Luego, Dapper sacaría 2 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 tuplas (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 intento otro método (a continuación), obtengo una excepción de "No se puede convertir un 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? Se parece al ejemplo de publicaciones / propietario, excepto que voy de padre a hijo en lugar de hijo a padre.

Gracias por adelantado

Jorin
fuente

Respuestas:

69

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

Entonces, esto funciona bien en muchos -> una dirección, pero menos bien en uno -> muchos mapas múltiples.

Aquí hay 2 problemas:

  1. Si introducimos un mapeador integrado 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 uno -> muchos, necesitaremos algún tipo de mapa de identidad. Lo que agrega complejidad.


Tomemos, por ejemplo, esta consulta que es eficiente si solo necesita extraer una cantidad limitada de registros, si empuja esto hasta un millón, las cosas se vuelven más complicadas, 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 GridReaderpara 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 mapeador:

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 salvedades. No me inclino a incluir esto en el núcleo.

Sam Saffron
fuente
Muy genial. Esta cosa tiene bastante poder ... Supongo que solo se está acostumbrando a cómo usarlo. Analizaré la carga útil de mis consultas y veré qué tan grandes son los conjuntos de resultados y veré si podemos permitirnos tener varias consultas y mapearlas juntas.
Jorin
@Jorin, tu otra opción sería organizar múltiples conexiones y tejer los resultados. Es un poco más complicado.
Sam Saffron
1
También agregaría un else después de if (childMap.TryGetvalue (..)) para que la colección secundaria se inicialice por defecto en una colección vacía en lugar de NULL si no hay elementos secundarios. Así: else {addChildren (item, new TChild [] {}); }
Marius
1
@SamSaffron Me encanta Dapper. Gracias. Aunque tengo una pregunta. Uno a muchos es una ocurrencia común en las consultas SQL. En el diseño, ¿qué tenía en mente para que lo usara el implementador? Quiero hacerlo de la forma Dapper, pero estoy en la forma SQL en este momento. ¿Cómo pienso que esto viene de SQL, donde One Side suele ser el "controlador"? ¿Por qué el lado de Muchos está tan en Dapper? ¿Es el punto para que obtengamos el objeto y analicemos después del hecho? Gracias por la gran biblioteca.
Johnny
2
Asegúrese de utilizar la herramienta adecuada para el trabajo. Si no tiene requisitos de rendimiento de base de datos masivos, o no ha evaluado su sistema, ha perdido horas o quizás días de su vida utilizando Dapper.
Aluan Haddad
32

Para su información, obtuve la respuesta de Sam funcionando haciendo lo siguiente:

Primero, agregué un archivo de clase llamado "Extensions.cs". Tuve que cambiar la palabra clave "this" a "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;
        }
    }
}

En segundo lugar, 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;
    }
}
Mike Gleason
fuente
24

Consulte https://www.tritac.com/blog/dappernet-by-example/ Podría hacer algo como esto:

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;

Obtuve esto de las pruebas de dapper.net: https://code.google.com/p/dapper-dot-net/source/browse/Tests/Tests.cs#1343

Jeroen K
fuente
2
¡Guauu! Para mí, encontré que esta es la solución más fácil. Concedido, para uno-> muchos, (asumiendo dos tablas), iría con las selecciones dobles. Sin embargo, en mi caso, tengo uno-> uno-> muchos y esto funciona muy bien. Ahora, devuelve una gran cantidad de datos redundantes, pero en mi caso, esta redundancia es relativamente pequeña: 10 filas en el mejor de los casos.
código5
Esto funciona bien para dos niveles, pero se vuelve complicado cuando tienes más.
Samir Aguiar
1
Si no hay datos secundarios, el código (s, a) se llamará con a = null, y las cuentas contendrán una lista con una entrada nula en lugar de estar vacía. Debe agregar "if (a! = Null)" antes de "shop.Accounts.Add (a)"
Etienne Charland
12

Compatibilidad con conjuntos de resultados múltiples

En su caso, sería mucho mejor (y también más fácil) tener una consulta de conjunto de resultados múltiples. Esto simplemente significa que debe escribir dos declaraciones de selección:

  1. Uno que devuelve contactos
  2. Y uno que devuelve sus números de teléfono

De esta manera, sus objetos serían únicos y no se duplicarían.

Robert Koritnik
fuente
1
Si bien las otras respuestas pueden ser elegantes a su manera, me gusta esta porque el código es más fácil de razonar. Puedo construir una jerarquía que tiene unos pocos niveles de profundidad con un puñado de declaraciones seleccionadas y aproximadamente 30 líneas de código foreach / linq. Esto podría fallar con conjuntos de resultados masivos, pero afortunadamente no tengo ese problema (todavía).
Sam Storie
10

Aquí hay una solución reutilizable que es bastante fácil de usar. Es una ligera modificación de la respuesta de 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;
}

Uso de ejemplo

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");
Arcilla
fuente
7

Basado en el enfoque de Sam Saffron (y Mike Gleason), aquí hay una solución que permitirá múltiples niños y múltiples niveles.

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

namespace TestMySQL.Helpers
{
    public static class Extensions
    {
        public static IEnumerable<TFirst> MapChild<TFirst, TSecond, TKey>
            (
            this SqlMapper.GridReader reader,
            List<TFirst> parent,
            List<TSecond> child,
            Func<TFirst, TKey> firstKey,
            Func<TSecond, TKey> secondKey,
            Action<TFirst, IEnumerable<TSecond>> addChildren
            )
        {
            var childMap = child
                .GroupBy(secondKey)
                .ToDictionary(g => g.Key, g => g.AsEnumerable());
            foreach (var item in parent)
            {
                IEnumerable<TSecond> children;
                if (childMap.TryGetValue(firstKey(item), out children))
                {
                    addChildren(item, children);
                }
            }
            return parent;
        }
    }
}

Entonces puede hacer que se lea fuera de la función.

using (var multi = conn.QueryMultiple(sql))
{
    var contactList = multi.Read<Contact>().ToList();
    var phoneList = multi.Read<Phone>().ToList;
    contactList = multi.MapChild
        (
            contactList,
            phoneList,
            contact => contact.Id, 
            phone => phone.ContactId,
            (contact, phone) => {contact.Phone = phone;}
        ).ToList();
    return contactList;
}

La función de mapa se puede volver a llamar para el siguiente objeto secundario utilizando el mismo objeto principal. También puede implementar divisiones en las sentencias de lectura principal o secundaria independientemente de la función de mapa.

Aquí hay un método de extensión adicional 'single to N'

    public static TFirst MapChildren<TFirst, TSecond, TKey>
        (
        this SqlMapper.GridReader reader,
        TFirst parent,
        IEnumerable<TSecond> children,
        Func<TFirst, TKey> firstKey,
        Func<TSecond, TKey> secondKey,
        Action<TFirst, IEnumerable<TSecond>> addChildren
        )
    {
        if (parent == null || children == null || !children.Any())
        {
            return parent;
        }

        Dictionary<TKey, IEnumerable<TSecond>> childMap = children
            .GroupBy(secondKey)
            .ToDictionary(g => g.Key, g => g.AsEnumerable());

        if (childMap.TryGetValue(firstKey(parent), out IEnumerable<TSecond> foundChildren))
        {
            addChildren(parent, foundChildren);
        }

        return parent;
    }
shlgug
fuente
2
Gracias por esta gran solución. eliminó la instrucción if para que en lugar de no llamar a addChilder en ningún hijo, la función de llamada pueda manejar los nulos. De esa manera puedo agregar listas vacías con las que es mucho más fácil trabajar.
Mladen Mihajlovic
1
Esta es una solución fantástica. Tuve algunos problemas con el "hallazgo dinámico". Eso se puede resolver con este contactList = multi.MapChild <Contact, Phone, int> (/ * mismo código que el anterior aquí * /
granadaCoder
4

Una vez que decidimos mover nuestro DataAccessLayer a procedimientos almacenados, estos procedimientos a menudo devuelven varios resultados vinculados (ejemplo a continuación).

Bueno, mi enfoque es casi el mismo, pero quizás un poco más cómodo.

Así es como puede verse su código:

using ( var conn = GetConn() )
{
    var res = await conn
        .StoredProc<Person>( procName, procParams )
        .Include<Book>( ( p, b ) => p.Books = b.Where( x => x.PersonId == p.Id ).ToList() )
        .Include<Course>( ( p, c ) => p.Courses = c.Where( x => x.PersonId == p.Id ).ToList() )
        .Include<Course, Mark>( ( c, m ) => c.Marks = m.Where( x => x.CourseId == c.Id ).ToList() )
        .Execute();
}


Vamos a analizarlo ...

Extensión:

public static class SqlExtensions
{
    public static StoredProcMapper<T> StoredProc<T>( this SqlConnection conn, string procName, object procParams )
    {
        return StoredProcMapper<T>
            .Create( conn )
            .Call( procName, procParams );
    }
}

Mapeador:

public class StoredProcMapper<T>
{
    public static StoredProcMapper<T> Create( SqlConnection conn )
    {
        return new StoredProcMapper<T>( conn );
    }

    private List<MergeInfo> _merges = new List<MergeInfo>();

    public SqlConnection Connection { get; }
    public string ProcName { get; private set; }
    public object Parameters { get; private set; }

    private StoredProcMapper( SqlConnection conn )
    {
        Connection = conn;
        _merges.Add( new MergeInfo( typeof( T ) ) );
    }

    public StoredProcMapper<T> Call( object procName, object parameters )
    {
        ProcName = procName.ToString();
        Parameters = parameters;

        return this;
    }

    public StoredProcMapper<T> Include<TChild>( MergeDelegate<T, TChild> mapper )
    {
        return Include<T, TChild>( mapper );
    }

    public StoredProcMapper<T> Include<TParent, TChild>( MergeDelegate<TParent, TChild> mapper )
    {
        _merges.Add( new MergeInfo<TParent, TChild>( mapper ) );
        return this;
    }

    public async Task<List<T>> Execute()
    {
        if ( string.IsNullOrEmpty( ProcName ) )
            throw new Exception( $"Procedure name not specified! Please use '{nameof(Call)}' method before '{nameof( Execute )}'" );

        var gridReader = await Connection.QueryMultipleAsync( 
            ProcName, Parameters, commandType: CommandType.StoredProcedure );

        foreach ( var merge in _merges )
        {
            merge.Result = gridReader
                .Read( merge.Type )
                .ToList();
        }

        foreach ( var merge in _merges )
        {
            if ( merge.ParentType == null )
                continue;

            var parentMerge = _merges.FirstOrDefault( x => x.Type == merge.ParentType );

            if ( parentMerge == null )
                throw new Exception( $"Wrong parent type '{merge.ParentType.FullName}' for type '{merge.Type.FullName}'." );

            foreach ( var parent in parentMerge.Result )
            {
                merge.Merge( parent, merge.Result );
            }
        }

        return _merges
            .First()
            .Result
            .Cast<T>()
            .ToList();
    }

    private class MergeInfo
    {
        public Type Type { get; }
        public Type ParentType { get; }
        public IEnumerable Result { get; set; }

        public MergeInfo( Type type, Type parentType = null )
        {
            Type = type;
            ParentType = parentType;
        }

        public void Merge( object parent, IEnumerable children )
        {
            MergeInternal( parent, children );
        }

        public virtual void MergeInternal( object parent, IEnumerable children )
        {

        }
    }

    private class MergeInfo<TParent, TChild> : MergeInfo
    {
        public MergeDelegate<TParent, TChild> Action { get; }

        public MergeInfo( MergeDelegate<TParent, TChild> mergeAction )
            : base( typeof( TChild ), typeof( TParent ) )
        {
            Action = mergeAction;
        }

        public override void MergeInternal( object parent, IEnumerable children )
        {
            Action( (TParent)parent, children.Cast<TChild>() );
        }
    }

    public delegate void MergeDelegate<TParent, TChild>( TParent parent, IEnumerable<TChild> children );
}

Eso es todo, pero si quieres hacer una prueba rápida, aquí tienes modelos y procedimientos para ti:

Modelos:

public class Person
{
    public Guid Id { get; set; }
    public string Name { get; set; }

    public List<Course> Courses { get; set; }
    public List<Book> Books { get; set; }

    public override string ToString() => Name;
}

public class Book
{
    public Guid Id { get; set; }
    public Guid PersonId { get; set; }
    public string Name { get; set; }

    public override string ToString() => Name;
}

public class Course
{
    public Guid Id { get; set; }
    public Guid PersonId { get; set; }
    public string Name { get; set; }

    public List<Mark> Marks { get; set; }

    public override string ToString() => Name;
}

public class Mark
{
    public Guid Id { get; set; }
    public Guid CourseId { get; set; }
    public int Value { get; set; }

    public override string ToString() => Value.ToString();
}

SP:

if exists ( 
    select * 
    from sysobjects 
    where  
        id = object_id(N'dbo.MultiTest')
        and ObjectProperty( id, N'IsProcedure' ) = 1 )
begin
    drop procedure dbo.MultiTest
end
go

create procedure dbo.MultiTest
    @PersonId UniqueIdentifier
as
begin

    declare @tmpPersons table 
    (
        Id UniqueIdentifier,
        Name nvarchar(50)
    );

    declare @tmpBooks table 
    (
        Id UniqueIdentifier,
        PersonId UniqueIdentifier,
        Name nvarchar(50)
    )

    declare @tmpCourses table 
    (
        Id UniqueIdentifier,
        PersonId UniqueIdentifier,
        Name nvarchar(50)
    )

    declare @tmpMarks table 
    (
        Id UniqueIdentifier,
        CourseId UniqueIdentifier,
        Value int
    )

--------------------------------------------------

    insert into @tmpPersons
    values
        ( '576fb8e8-41a2-43a9-8e77-a8213aa6e387', N'Иван' ),
        ( '467953a5-cb5f-4d06-9fad-505b3bba2058', N'Василий' ),
        ( '52a719bf-6f1f-48ac-9e1f-4532cfc70d96', N'Алефтина' )


    insert into @tmpBooks
    values
        ( NewId(), '576fb8e8-41a2-43a9-8e77-a8213aa6e387', N'Книга Математика' ),
        ( NewId(), '576fb8e8-41a2-43a9-8e77-a8213aa6e387', N'Книга Физика' ),
        ( NewId(), '576fb8e8-41a2-43a9-8e77-a8213aa6e387', N'Книга Геометрия' ),

        ( NewId(), '467953a5-cb5f-4d06-9fad-505b3bba2058', N'Книга Биология' ),
        ( NewId(), '467953a5-cb5f-4d06-9fad-505b3bba2058', N'Книга Химия' ),

        ( NewId(), '52a719bf-6f1f-48ac-9e1f-4532cfc70d96', N'Книга История' ),
        ( NewId(), '52a719bf-6f1f-48ac-9e1f-4532cfc70d96', N'Книга Литература' ),
        ( NewId(), '52a719bf-6f1f-48ac-9e1f-4532cfc70d96', N'Книга Древне-шумерский диалект иврита' )


    insert into @tmpCourses
    values
        ( '30945b68-a6ef-4da8-9a35-d3b2845e7de3', '576fb8e8-41a2-43a9-8e77-a8213aa6e387', N'Математика' ),
        ( '7881f090-ccd6-4fb9-a1e0-ff4ff5c18450', '576fb8e8-41a2-43a9-8e77-a8213aa6e387', N'Физика' ),
        ( '92bbefd1-9fec-4dc7-bb58-986eadb105c8', '576fb8e8-41a2-43a9-8e77-a8213aa6e387', N'Геометрия' ),

        ( '923a2f0c-c5c7-4394-847c-c5028fe14711', '467953a5-cb5f-4d06-9fad-505b3bba2058', N'Биология' ),
        ( 'ace50388-eb05-4c46-82a9-5836cf0c988c', '467953a5-cb5f-4d06-9fad-505b3bba2058', N'Химия' ),

        ( '53ea69fb-6cc4-4a6f-82c2-0afbaa8cb410', '52a719bf-6f1f-48ac-9e1f-4532cfc70d96', N'История' ),
        ( '7290c5f7-1000-4f44-a5f0-6a7cf8a8efab', '52a719bf-6f1f-48ac-9e1f-4532cfc70d96', N'Литература' ),
        ( '73ac366d-c7c2-4480-9513-28c17967db1a', '52a719bf-6f1f-48ac-9e1f-4532cfc70d96', N'Древне-шумерский диалект иврита' )

    insert into @tmpMarks
    values
        ( NewId(), '30945b68-a6ef-4da8-9a35-d3b2845e7de3', 98 ),
        ( NewId(), '30945b68-a6ef-4da8-9a35-d3b2845e7de3', 87 ),
        ( NewId(), '30945b68-a6ef-4da8-9a35-d3b2845e7de3', 76 ),

        ( NewId(), '7881f090-ccd6-4fb9-a1e0-ff4ff5c18450', 89 ),
        ( NewId(), '7881f090-ccd6-4fb9-a1e0-ff4ff5c18450', 78 ),
        ( NewId(), '7881f090-ccd6-4fb9-a1e0-ff4ff5c18450', 67 ),

        ( NewId(), '92bbefd1-9fec-4dc7-bb58-986eadb105c8', 79 ),
        ( NewId(), '92bbefd1-9fec-4dc7-bb58-986eadb105c8', 68 ),
        ( NewId(), '92bbefd1-9fec-4dc7-bb58-986eadb105c8', 75 ),
        ----------
        ( NewId(), '923a2f0c-c5c7-4394-847c-c5028fe14711', 198 ),
        ( NewId(), '923a2f0c-c5c7-4394-847c-c5028fe14711', 187 ),
        ( NewId(), '923a2f0c-c5c7-4394-847c-c5028fe14711', 176 ),

        ( NewId(), 'ace50388-eb05-4c46-82a9-5836cf0c988c', 189 ),
        ( NewId(), 'ace50388-eb05-4c46-82a9-5836cf0c988c', 178 ),
        ( NewId(), 'ace50388-eb05-4c46-82a9-5836cf0c988c', 167 ),
        ----------
        ( NewId(), '53ea69fb-6cc4-4a6f-82c2-0afbaa8cb410', 8 ),
        ( NewId(), '53ea69fb-6cc4-4a6f-82c2-0afbaa8cb410', 7 ),
        ( NewId(), '53ea69fb-6cc4-4a6f-82c2-0afbaa8cb410', 6 ),

        ( NewId(), '7290c5f7-1000-4f44-a5f0-6a7cf8a8efab', 9 ),
        ( NewId(), '7290c5f7-1000-4f44-a5f0-6a7cf8a8efab', 8 ),
        ( NewId(), '7290c5f7-1000-4f44-a5f0-6a7cf8a8efab', 7 ),

        ( NewId(), '73ac366d-c7c2-4480-9513-28c17967db1a', 9 ),
        ( NewId(), '73ac366d-c7c2-4480-9513-28c17967db1a', 8 ),
        ( NewId(), '73ac366d-c7c2-4480-9513-28c17967db1a', 5 )

--------------------------------------------------

    select * from @tmpPersons
    select * from @tmpBooks
    select * from @tmpCourses
    select * from @tmpMarks

end
go
Sam Sch
fuente
1
No sé por qué este enfoque no ha recibido atención o comentarios hasta ahora, pero lo encuentro muy interesante y lógicamente estructurado. Gracias por compartir. Creo que puede aplicar este enfoque a funciones con valores de tabla o incluso a cadenas de SQL, simplemente difieren en el tipo de comando. Solo algunas extensiones / sobrecargas y esto debería funcionar para todos los tipos de consultas comunes.
Grimm
para asegurarme de que estoy leyendo esto correctamente, esto requiere que el usuario sepa exactamente en qué orden de tipo devolverá los resultados el procedimiento, ¿es así? Si intercambia Incluir <Libro> e Incluir <Curso>, por ejemplo, esto arrojaría?
cubesnyc
@cubesnyc no recuerdo si lanza, pero sí, el usuario debe conocer el pedido
Sam Sch
2

Quería compartir mi solución a este problema y ver si alguien tiene algún comentario constructivo sobre el enfoque que he utilizado.

Tengo algunos requisitos en el proyecto en el que estoy trabajando que necesito explicar primero:

  1. Tengo que mantener mis POCO lo más limpios posible, ya que estas clases se compartirán públicamente en un contenedor de API.
  2. Mis POCO están en una biblioteca de clases separada debido al requisito anterior
  3. Habrá varios niveles de jerarquía de objetos que variarán según los datos (por lo que no puedo usar un asignador de tipos genéricos o tendría que escribir toneladas de ellos para atender todas las eventualidades posibles)

Entonces, lo que he hecho es hacer que SQL maneje la jerarquía del segundo nivel al devolver una cadena JSON única como una columna en la fila original de la siguiente manera ( eliminó las otras columnas / propiedades, etc. para ilustrar ):

Id  AttributeJson
4   [{Id:1,Name:"ATT-NAME",Value:"ATT-VALUE-1"}]

Luego, mis POCO se construyen como se muestra a continuación:

public abstract class BaseEntity
{
    [KeyAttribute]
    public int Id { get; set; }
}

public class Client : BaseEntity
{
    public List<ClientAttribute> Attributes{ get; set; }
}
public class ClientAttribute : BaseEntity
{
    public string Name { get; set; }
    public string Value { get; set; }
}

Donde los POCO heredan de BaseEntity. (Para ilustrar, he elegido una jerarquía de un solo nivel bastante simple, como se muestra en la propiedad "Atributos" del objeto cliente).

Entonces tengo en mi capa de datos la siguiente "Clase de datos" que hereda del POCO Client.

internal class dataClient : Client
{
    public string AttributeJson
    {
        set
        {
            Attributes = value.FromJson<List<ClientAttribute>>();
        }
    }
}

Como puede ver arriba, lo que sucede es que SQL está devolviendo una columna llamada "AttributeJson" que se asigna a la propiedad AttributeJson en la clase dataClient. Esto solo tiene un establecedor que deserializa el JSON a la Attributespropiedad de la Clientclase heredada . La clase dataClient es internalpara la capa de acceso a datos y ClientProvider(mi fábrica de datos) devuelve el POCO del cliente original a la aplicación / biblioteca que llama así:

var clients = _conn.Get<dataClient>();
return clients.OfType<Client>().ToList();

Tenga en cuenta que estoy usando Dapper.Contrib y he agregado un nuevo Get<T>método que devuelve unIEnumerable<T>

Hay un par de cosas a tener en cuenta con esta solución:

  1. Hay una compensación de rendimiento obvia con la serialización JSON: lo comparé con 1050 filas con 2 List<T>subpropiedades, cada una con 2 entidades en la lista y marca 279 ms, lo cual es aceptable para las necesidades de mis proyectos, esto también es Optimización CERO en el lado SQL de las cosas, por lo que debería poder afeitarme unos pocos ms allí.

  2. Significa que se requieren consultas SQL adicionales para construir el JSON para cada requisito List<T> propiedad , pero nuevamente, esto me conviene ya que conozco SQL bastante bien y no soy tan fluido en dinámica / reflexión, etc., así que de esta manera siento que tengo más control sobre las cosas, ya que realmente entiendo lo que está sucediendo debajo del capó :-)

Es posible que haya una solución mejor que esta y, si la hay, realmente agradecería escuchar sus pensamientos; esta es solo la solución que se me ocurrió y que hasta ahora se ajusta a mis necesidades para este proyecto (aunque esto es experimental en la etapa de publicación ).

Dave Long
fuente
Esto es interesante. ¿Alguna posibilidad de que pueda compartir la parte SQL?
WhiteRuski