Escribí este código para proyectar una relación de uno a muchos, pero no funciona:
using (var connection = new SqlConnection(connectionString))
{
connection.Open();
IEnumerable<Store> stores = connection.Query<Store, IEnumerable<Employee>, Store>
(@"Select Stores.Id as StoreId, Stores.Name,
Employees.Id as EmployeeId, Employees.FirstName,
Employees.LastName, Employees.StoreId
from Store Stores
INNER JOIN Employee Employees ON Stores.Id = Employees.StoreId",
(a, s) => { a.Employees = s; return a; },
splitOn: "EmployeeId");
foreach (var store in stores)
{
Console.WriteLine(store.Name);
}
}
¿Alguien puede detectar el error?
EDITAR:
Estas son mis entidades:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public double Price { get; set; }
public IList<Store> Stores { get; set; }
public Product()
{
Stores = new List<Store>();
}
}
public class Store
{
public int Id { get; set; }
public string Name { get; set; }
public IEnumerable<Product> Products { get; set; }
public IEnumerable<Employee> Employees { get; set; }
public Store()
{
Products = new List<Product>();
Employees = new List<Employee>();
}
}
EDITAR:
Cambio la consulta a:
IEnumerable<Store> stores = connection.Query<Store, List<Employee>, Store>
(@"Select Stores.Id as StoreId ,Stores.Name,Employees.Id as EmployeeId,
Employees.FirstName,Employees.LastName,Employees.StoreId
from Store Stores INNER JOIN Employee Employees
ON Stores.Id = Employees.StoreId",
(a, s) => { a.Employees = s; return a; }, splitOn: "EmployeeId");
¡y me deshago de las excepciones! Sin embargo, los empleados no se asignan en absoluto. Todavía no estoy seguro de qué problema tuvo IEnumerable<Employee>
en la primera consulta.
Respuestas:
Esta publicación muestra cómo consultar una base de datos SQL altamente normalizada y mapear el resultado en un conjunto de objetos C # POCO altamente anidados.
Ingredientes:
La idea que me permitió resolver este problema es separar el
MicroORM
demapping the result back to the POCO Entities
. Por lo tanto, usamos dos bibliotecas separadas:Esencialmente, usamos Dapper para consultar la base de datos, luego usamos Slapper.Automapper para mapear el resultado directamente en nuestros POCO.
Ventajas
List<MyClass1>
que a su vez contieneList<MySubClass2>
, etc.).inner joins
para devolver resultados planos es mucho más fácil que crear múltiples declaraciones de selección, con uniones en el lado del cliente.Desventajas
inner join
(que trae duplicados), en su lugar debemos usar múltiplesselect
declaraciones y unir todo de nuevo en el lado del cliente (vea las otras respuestas en esta página).Pruebas de rendimiento
En mis pruebas, Slapper.Automapper agregó una pequeña sobrecarga a los resultados devueltos por Dapper, lo que significaba que todavía era 10 veces más rápido que Entity Framework, y la combinación todavía está bastante cerca de la velocidad máxima teórica que SQL + C # es capaz de hacer .
En la mayoría de los casos prácticos, la mayor parte de la sobrecarga se produciría en una consulta SQL menos que óptima y no con una asignación de los resultados en el lado C #.
Resultados de las pruebas de rendimiento
Número total de iteraciones: 1000
Dapper by itself
: 1.889 milisegundos por consulta, usando3 lines of code to return the dynamic
.Dapper + Slapper.Automapper
: 2.463 milisegundos por consulta, usando un adicional3 lines of code for the query + mapping from dynamic to POCO Entities
.Ejemplo resuelto
En este ejemplo, tenemos una lista de
Contacts
y cada unoContact
puede tener uno o másphone numbers
.Entidades POCO
public class TestContact { public int ContactID { get; set; } public string ContactName { get; set; } public List<TestPhone> TestPhones { get; set; } } public class TestPhone { public int PhoneId { get; set; } public int ContactID { get; set; } // foreign key public string Number { get; set; } }
Tabla SQL
TestContact
Tabla SQL
TestPhone
Tenga en cuenta que esta tabla tiene una clave externa
ContactID
que se refiere a laTestContact
tabla (esto corresponde a laList<TestPhone>
del POCO anterior).SQL que produce un resultado plano
En nuestra consulta SQL, usamos tantas
JOIN
declaraciones como necesitemos para obtener todos los datos que necesitamos, en una forma plana y desnormalizada . Sí, esto puede producir duplicados en la salida, pero estos duplicados se eliminarán automáticamente cuando usemos Slapper.Automapper para mapear automáticamente el resultado de esta consulta directamente en nuestro mapa de objetos POCO.USE [MyDatabase]; SELECT tc.[ContactID] as ContactID ,tc.[ContactName] as ContactName ,tp.[PhoneId] AS TestPhones_PhoneId ,tp.[ContactId] AS TestPhones_ContactId ,tp.[Number] AS TestPhones_Number FROM TestContact tc INNER JOIN TestPhone tp ON tc.ContactId = tp.ContactId
Código C #
const string sql = @"SELECT tc.[ContactID] as ContactID ,tc.[ContactName] as ContactName ,tp.[PhoneId] AS TestPhones_PhoneId ,tp.[ContactId] AS TestPhones_ContactId ,tp.[Number] AS TestPhones_Number FROM TestContact tc INNER JOIN TestPhone tp ON tc.ContactId = tp.ContactId"; string connectionString = // -- Insert SQL connection string here. using (var conn = new SqlConnection(connectionString)) { conn.Open(); // Can set default database here with conn.ChangeDatabase(...) { // Step 1: Use Dapper to return the flat result as a Dynamic. dynamic test = conn.Query<dynamic>(sql); // Step 2: Use Slapper.Automapper for mapping to the POCO Entities. // - IMPORTANT: Let Slapper.Automapper know how to do the mapping; // let it know the primary key for each POCO. // - Must also use underscore notation ("_") to name parameters in the SQL query; // see Slapper.Automapper docs. Slapper.AutoMapper.Configuration.AddIdentifiers(typeof(TestContact), new List<string> { "ContactID" }); Slapper.AutoMapper.Configuration.AddIdentifiers(typeof(TestPhone), new List<string> { "PhoneID" }); var testContact = (Slapper.AutoMapper.MapDynamic<TestContact>(test) as IEnumerable<TestContact>).ToList(); foreach (var c in testContact) { foreach (var p in c.TestPhones) { Console.Write("ContactName: {0}: Phone: {1}\n", c.ContactName, p.Number); } } } }
Salida
Jerarquía de entidades de POCO
Mirando en Visual Studio, podemos ver que Slapper.Automapper ha poblado correctamente nuestras Entidades POCO, es decir, tenemos un
List<TestContact>
, y cada unoTestContact
tiene unList<TestPhone>
.Notas
Tanto Dapper como Slapper.Automapper almacena todo internamente para mayor velocidad. Si tiene problemas de memoria (muy poco probable), asegúrese de borrar ocasionalmente la memoria caché de ambos.
Asegúrese de nombrar las columnas que regresan, usando la notación de subrayado (
_
) para darle a Slapper.Automapper pistas sobre cómo mapear el resultado en las Entidades POCO.Asegúrese de proporcionar pistas a Slapper.Automapper sobre la clave principal de cada entidad POCO (consulte las líneas
Slapper.AutoMapper.Configuration.AddIdentifiers
). También puede usarAttributes
en POCO para esto. Si omite este paso, podría salir mal (en teoría), ya que Slapper.Automapper no sabría cómo hacer el mapeo correctamente.Actualización 2015-06-14
Aplicó con éxito esta técnica a una enorme base de datos de producción con más de 40 tablas normalizadas. Funcionó perfectamente para mapear una consulta SQL avanzada con más de 16
inner join
yleft join
en la jerarquía de POCO adecuada (con 4 niveles de anidación). Las consultas son increíblemente rápidas, casi tan rápidas como codificarlas manualmente en ADO.NET (normalmente eran 52 milisegundos para la consulta y 50 milisegundos para la asignación del resultado plano a la jerarquía POCO). Esto no es realmente nada revolucionario, pero seguro que supera a Entity Framework en velocidad y facilidad de uso, especialmente si todo lo que estamos haciendo es ejecutar consultas.Actualización 2016-02-19
Code ha estado funcionando sin problemas en producción durante 9 meses. La última versión de
Slapper.Automapper
tiene todos los cambios que apliqué para solucionar el problema relacionado con la devolución de nulos en la consulta SQL.Actualización 2017-02-20
El código se ha estado ejecutando sin problemas en producción durante 21 meses y ha manejado consultas continuas de cientos de usuarios en una empresa FTSE 250.
Slapper.Automapper
también es ideal para mapear un archivo .csv directamente en una lista de POCO. Lea el archivo .csv en una lista de IDictionary, luego asócielo directamente en la lista de destino de POCO. El único truco es que debe agregar una propiedadint Id {get; set}
y asegurarse de que sea única para cada fila (de lo contrario, el automapper no podrá distinguir entre las filas).Actualización 2019-01-29
Actualización menor para agregar más comentarios de código.
Ver: https://github.com/SlapperAutoMapper/Slapper.AutoMapper
fuente
Quería mantenerlo lo más simple posible, mi solución:
public List<ForumMessage> GetForumMessagesByParentId(int parentId) { var sql = @" select d.id_data as Id, d.cd_group As GroupId, d.cd_user as UserId, d.tx_login As Login, d.tx_title As Title, d.tx_message As [Message], d.tx_signature As [Signature], d.nm_views As Views, d.nm_replies As Replies, d.dt_created As CreatedDate, d.dt_lastreply As LastReplyDate, d.dt_edited As EditedDate, d.tx_key As [Key] from t_data d where d.cd_data = @DataId order by id_data asc; select d.id_data As DataId, di.id_data_image As DataImageId, di.cd_image As ImageId, i.fl_local As IsLocal from t_data d inner join T_data_image di on d.id_data = di.cd_data inner join T_image i on di.cd_image = i.id_image where d.id_data = @DataId and di.fl_deleted = 0 order by d.id_data asc;"; var mapper = _conn.QueryMultiple(sql, new { DataId = parentId }); var messages = mapper.Read<ForumMessage>().ToDictionary(k => k.Id, v => v); var images = mapper.Read<ForumMessageImage>().ToList(); foreach(var imageGroup in images.GroupBy(g => g.DataId)) { messages[imageGroup.Key].Images = imageGroup.ToList(); } return messages.Values.ToList(); }
Todavía hago una llamada a la base de datos, y aunque ahora ejecuto 2 consultas en lugar de una, la segunda consulta está usando una combinación INNER en lugar de una combinación IZQUIERDA menos óptima.
fuente
.Join(
pero produce un gráfico de objetos en lugar de un resultado plano.Una ligera modificación de la respuesta de Andrew que utiliza un Func para seleccionar la clave principal en lugar de
GetHashCode
.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
conn.QueryParentChild<Product, Store, int>("sql here", prod => prod.Id, prod => prod.Stores)
fuente
class Parent { public List<Child> Children { get; set; } public Parent() { this.Children = new List<Child>(); } }
De acuerdo con esta respuesta, no hay soporte de mapeo de uno a muchos integrado en Dapper.Net. Las consultas siempre devolverán un objeto por fila de la base de datos. Sin embargo, se incluye una solución alternativa.
fuente
(contact, phones) => { contact.Phones = phones; }
Tendría que escribir un filtro para teléfonos cuyo contactid coincida con el contactid del contacto. Esto es bastante ineficiente.Aquí hay una solución cruda
public static IEnumerable<TOne> Query<TOne, TMany>(this IDbConnection cnn, string sql, Func<TOne, IList<TMany>> property, dynamic param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null) { var cache = new Dictionary<int, TOne>(); cnn.Query<TOne, TMany, TOne>(sql, (one, many) => { if (!cache.ContainsKey(one.GetHashCode())) cache.Add(one.GetHashCode(), one); var localOne = cache[one.GetHashCode()]; var list = property(localOne); list.Add(many); return localOne; }, param as object, transaction, buffered, splitOn, commandTimeout, commandType); return cache.Values; }
de ninguna manera es la forma más eficiente, pero lo pondrá en funcionamiento. Intentaré optimizar esto cuando tenga la oportunidad.
úsalo así:
conn.Query<Product, Store>("sql here", prod => prod.Stores);
tenga en cuenta que sus objetos necesitan implementar
GetHashCode
, tal vez así:public override int GetHashCode() { return this.Id.GetHashCode(); }
fuente
Aquí hay otro método:
Pedido (uno) - OrderDetail (muchos)
using (var connection = new SqlCeConnection(connectionString)) { var orderDictionary = new Dictionary<int, Order>(); var list = connection.Query<Order, OrderDetail, Order>( sql, (order, orderDetail) => { Order orderEntry; if (!orderDictionary.TryGetValue(order.OrderID, out orderEntry)) { orderEntry = order; orderEntry.OrderDetails = new List<OrderDetail>(); orderDictionary.Add(orderEntry.OrderID, orderEntry); } orderEntry.OrderDetails.Add(orderDetail); return orderEntry; }, splitOn: "OrderDetailID") .Distinct() .ToList(); }
Fuente : http://dapper-tutorial.net/result-multi-mapping#example---query-multi-mapping-one-to-many
fuente