Pregunta

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 están asignados en absoluto.Todavía no estoy seguro de qué problema tuvo IEnumerable<Employee> en la primera consulta.

¿Fue útil?

Solución

Esta publicación muestra cómo consultar un base de datos SQL altamente normalizada, y asigne el resultado a un conjunto de objetos C# POCO altamente anidados.

Ingredientes:

  • 8 líneas de C#.
  • Algún SQL razonablemente simple que utiliza algunas combinaciones.
  • Dos bibliotecas impresionantes.

La idea que me permitió resolver este problema es separar los MicroORM de mapping the result back to the POCO Entities.Por lo tanto, utilizamos dos bibliotecas separadas:

Básicamente utilizamos Apuesto para consultar la base de datos, luego use Slapper.Automapper para asignar el resultado directamente a nuestros POCO.

Ventajas

  • Sencillez.Son menos de 8 líneas de código.Esto me parece mucho más fácil de entender, depurar y cambiar.
  • Menos código.Unas pocas líneas de código es todo. Slapper.Automapper necesita manejar cualquier cosa que le arrojes, incluso si tenemos un POCO anidado complejo (es decir,POCO contiene List<MyClass1> que a su vez contiene List<MySubClass2>, etc).
  • Velocidad.Ambas bibliotecas tienen una extraordinaria cantidad de optimización y almacenamiento en caché para que se ejecuten casi tan rápido como las consultas ADO.NET ajustadas manualmente.
  • Separación de intereses.Podemos cambiar el MicroORM por otro diferente y el mapeo seguirá funcionando, y viceversa.
  • Flexibilidad. Slapper.Automapper maneja jerarquías anidadas arbitrariamente, no se limita a un par de niveles de anidación.Podemos realizar cambios rápidos fácilmente y todo seguirá funcionando.
  • Depuración.Primero podemos ver que la consulta SQL funciona correctamente, luego podemos verificar que el resultado de la consulta SQL esté asignado correctamente a las entidades POCO de destino.
  • Facilidad de desarrollo en SQL.Encuentro que crear consultas aplanadas con inner joins devolver resultados planos es mucho más fácil que crear múltiples declaraciones de selección, con uniones en el lado del cliente.
  • Consultas optimizadas en SQL.En una base de datos altamente normalizada, la creación de una consulta plana permite que el motor SQL aplique optimizaciones avanzadas al conjunto, lo que normalmente no sería posible si se construyeran y ejecutaran muchas consultas individuales pequeñas.
  • Confianza.Dapper es el backend de StackOverflow y, bueno, Randy Burden es una especie de superestrella.¿Necesito decir algo más?
  • Velocidad de desarrollo. Pude realizar algunas consultas extraordinariamente complejas, con muchos niveles de anidamiento, y el tiempo de desarrollo fue bastante bajo.
  • Menos errores. Lo escribí una vez, simplemente funcionó y esta técnica ahora está ayudando a impulsar una empresa del FTSE.Había tan poco código que no hubo ningún comportamiento inesperado.

Desventajas

  • Se devolvió una escala más allá de 1.000.000 de filas. Funciona bien cuando se devuelven <100.000 filas.Sin embargo, si recuperamos >1.000.000 de filas, para reducir el tráfico entre nosotros y el servidor SQL, no debemos aplanarlo usando inner join (que devuelve duplicados), en su lugar deberíamos usar múltiples select declaraciones y volver a unir todo en el lado del cliente (consulte las otras respuestas en esta página).
  • Esta técnica está orientada a consultas..No he usado esta técnica para escribir en la base de datos, pero estoy seguro de que Dapper es más que capaz de hacer esto con un poco más de trabajo adicional, ya que StackOverflow usa Dapper como su capa de acceso a datos (DAL).

Pruebas de rendimiento

En mis pruebas, Slapper.Automapper agregó una pequeña sobrecarga a los resultados arrojados 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 alcanzar.

En la mayoría de los casos prácticos, la mayor parte de la sobrecarga se produciría en una consulta SQL no óptima y no en algún mapeo de los resultados en el lado de C#.

Resultados de las pruebas de rendimiento

Número total de iteraciones:1000

  • Dapper by itself: 1.889 milisegundos por consulta, usando 3 lines of code to return the dynamic.
  • Dapper + Slapper.Automapper: 2.463 milisegundos por consulta, utilizando un adicional 3 lines of code for the query + mapping from dynamic to POCO Entities.

Ejemplo resuelto

En este ejemplo, tenemos una lista de Contacts, y cada Contact puede tener uno o más phone 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

enter image description here

Tabla SQL TestPhone

Tenga en cuenta que esta tabla tiene una clave externa. ContactID que se refiere a la TestContact tabla (esto corresponde a la List<TestPhone> en POCO arriba).

enter image description here

SQL que produce resultados planos

En nuestra consulta SQL, utilizamos tantos JOIN declaraciones ya que necesitamos obtener todos los datos que necesitamos, en un forma plana y desnormalizada.Sí, esto podría producir duplicados en la salida, pero estos duplicados se eliminarán automáticamente cuando usemos Slapper.Automapper para asignar automáticamente el resultado de esta consulta directamente a 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

enter image description here

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

Producción

enter image description here

Jerarquía de entidades de POCO

Al mirar en Visual Studio, podemos ver que Slapper.Automapper ha poblado correctamente nuestras entidades POCO, es decir,tenemos una List<TestContact>, y cada TestContact tiene un List<TestPhone>.

enter image description here

Notas

Tanto Dapper como Slapper.Automapper almacenan en caché todo internamente para mayor velocidad.Si tiene problemas de memoria (muy poco probable), asegúrese de borrar ocasionalmente el caché de ambos.

Asegúrese de nombrar las columnas que regresan, usando el guion bajo (_) notación para darle pistas a Slapper.Automapper sobre cómo asignar el resultado a las Entidades POCO.

Asegúrese de darle pistas a Slapper.Automapper sobre la clave principal para cada entidad POCO (consulte las líneas Slapper.AutoMapper.Configuration.AddIdentifiers).También puedes usar Attributes en POCO para esto.Si omites este paso, podría salir mal (en teoría), ya que Slapper.Automapper no sabría cómo realizar 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 y left join en la jerarquía POCO adecuada (con 4 niveles de anidamiento).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 el mapeo del resultado plano a la jerarquía POCO).Esto realmente no es nada revolucionario, pero seguro que supera a Entity Framework en velocidad y facilidad de uso, especialmente si lo único que hacemos es ejecutar consultas.

Actualización 2016-02-19

El código 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 los valores nulos que se devuelven en la consulta SQL.

Actualización 2017-02-20

El código ha estado funcionando sin problemas en producción durante 21 meses y ha manejado consultas continuas de cientos de usuarios en una empresa del FTSE 250.

Slapper.Automapper También es excelente para asignar un archivo .csv directamente a una lista de POCO.Lea el archivo .csv en una lista de IDictionary, luego asígnelo directamente a la lista de destino de POCO.El único truco es que tienes que agregar una propiedad. int Id {get; set}, y asegúrese de que sea único para cada fila (de lo contrario, el asignador automático 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

Otros consejos

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 es usar una unión interna en lugar de una unión izquierda menos óptima.

De acuerdo a esta respuesta No hay nadie a muchos soporte de mapeo integrado en dapper.net. Las consultas siempre devolverán un objeto por fila de base de datos. Sin embargo, hay una solución alternativa incluida.

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)

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 te pondrá en funcionamiento. Intentaré optimizar esto cuando tenga la oportunidad.

Úselo así:

conn.Query<Product, Store>("sql here", prod => prod.Stores);

Tenga en cuenta que sus objetos deben implementar GetHashCode, tal vez así:

    public override int GetHashCode()
    {
        return this.Id.GetHashCode();
    }

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

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top