Multi-Mapper para crear jerarquía de objetos

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 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>("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, 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

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

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

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

Suponiendo que extiende su GridReader y con un asignador:

 public static IEnumerable Map ( this GridReader reader, Func firstKey, Func secondKey, Action> addChildren ) { var first = reader.Read().ToList(); var childMap = reader .Read() .GroupBy(s => secondKey(s)) .ToDictionary(g => g.Key, g => g.AsEnumerable()); foreach (var item in first) { IEnumerable 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.

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 Map ( this Dapper.SqlMapper.GridReader reader, Func firstKey, Func secondKey, Action> addChildren ) { var first = reader.Read().ToList(); var childMap = reader .Read() .GroupBy(s => secondKey(s)) .ToDictionary(g => g.Key, g => g.AsEnumerable()); foreach (var item in first) { IEnumerable 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 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 => contact.ContactID, phone => phone.ContactID, (contact, phones) => { contact.Phones = phones; } ); return mapped; } } 

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

 public class Shop { public int? Id {get;set;} public string Name {get;set;} public string Url {get;set;} public IList 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() conn.Query(@" 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; 

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

Soporte de conjunto de resultados múltiples

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

  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.

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 QueryParentChild( this IDbConnection connection, string sql, Func parentKeySelector, Func> childSelector, dynamic param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null) { Dictionary cache = new Dictionary(); connection.Query( sql, (parent, child) => { if (!cache.ContainsKey(parentKeySelector(parent))) { cache.Add(parentKeySelector(parent), parent); } TParent cachedParent = cache[parentKeySelector(parent)]; IList children = childSelector(cachedParent); children.Add(child); return cachedParent; }, param as object, transaction, buffered, splitOn, commandTimeout, commandType); return cache.Values; } 

Ejemplo de uso

 public class Contact { public int ContactID { get; set; } public string ContactName { get; set; } public List Phones { get; set; } // must be IList public Contact() { this.Phones = new List(); // 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( "SELECT * FROM Contact LEFT OUTER JOIN Phone ON Contact.ContactID = Phone.ContactID", contact => contact.ContactID, contact => contact.Phones, splitOn: "PhoneId"); 

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 MapChild ( this SqlMapper.GridReader reader, List parent, List child, Func firstKey, Func secondKey, Action> addChildren ) { var childMap = child .GroupBy(secondKey) .ToDictionary(g => g.Key, g => g.AsEnumerable()); foreach (var item in parent) { IEnumerable children; if (childMap.TryGetValue(firstKey(item), out children)) { addChildren(item, children); } } return parent; } } } 

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

 using (var multi = conn.QueryMultiple(sql)) { var contactList = multi.Read().ToList(); var phoneList = multi.Read().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 próximo objeto secundario utilizando el mismo objeto principal. También puede implementar divisiones en las instrucciones leídas padre o hijo independientemente de la función de mapa.

Quería compartir mi solución a este problema y ver si alguien tiene comentarios constructivos sobre el enfoque que he utilizado.

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

  1. Debo mantener mi POCO lo más limpio posible ya que estas clases se compartirán públicamente en un contenedor 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 Mapper de tipo genérico o tendré que escribir toneladas para atender todas las eventualidades posibles)

Por lo tanto, lo que he hecho es hacer que SQL maneje la jerarquía de nivel 2nd – nth devolviendo una cadena JSON única como columna en la fila original de la siguiente manera ( quitando las otras columnas / propiedades, etc. para ilustrar ):

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

Entonces, mis POCO se construyen como el siguiente:

 public abstract class BaseEntity { [KeyAttribute] public int Id { get; set; } } public class Client : BaseEntity { public List Attributes{ get; set; } } public class ClientAttribute : BaseEntity { public string Name { get; set; } public string Value { get; set; } } 

Donde el POCO hereda 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).

Luego tengo en mi capa de datos la siguiente “clase de datos” que hereda del Client POCO.

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

Como puede ver arriba, lo que sucede es que SQL está devolviendo una columna llamada “AttributeJson” que está mapeada a la propiedad AttributeJson en la clase dataClient. Esto tiene solo un setter que deserializa el JSON a la propiedad de Attributes en la clase de Client heredada. La clase DataClient es internal a la capa de acceso a datos y ClientProvider (mi fábrica de datos) devuelve el POCO cliente original a la aplicación / biblioteca que realiza la llamada, así:

 var clients = _conn.Get(); return clients.OfType().ToList(); 

Tenga en cuenta que estoy usando Dapper.Contrib y he agregado un nuevo método Get que devuelve un IEnumerable

Hay algunas cosas que destacar con esta solución:

  1. Hay una compensación de rendimiento obvia con la serialización de JSON: comparé esto con 1050 filas con 2 propiedades de la sub- List , cada una con 2 entidades en la lista y alcanza los 279 ms, lo cual es aceptable para las necesidades de mis proyectos. esto también es con la optimización CERO en el lado SQL de las cosas, así que debería poder afeitarme unos pocos ms.

  2. Significa que se requieren consultas SQL adicionales para construir el JSON para cada propiedad requerida de la List , pero de nuevo, esto me conviene porque sé 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 sucede debajo del capó 🙂

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