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:
- 8 líneas de C #.
- Algún SQL razonablemente simple que usa algunas combinaciones.
- Dos fantásticas bibliotecas.
La idea que me permitió resolver este problema es separar el MicroORM
de mapping 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
- Sencillez . Tiene menos de 8 líneas de código. Encuentro esto mucho más fácil de entender, depurar y cambiar.
- Menos código . Unas pocas líneas de código son 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 cantidad extraordinaria de optimización y almacenamiento en caché para que se ejecuten casi tan rápido como las consultas ADO.NET ajustadas a mano.
- Separación de preocupaciones . Podemos cambiar el MicroORM por uno diferente, y el mapeo aún funciona, y viceversa.
- Flexibilidad . Slapper.Automapper maneja jerarquías anidadas arbitrariamente, no se limita a un par de niveles de anidación. Podemos hacer 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é correctamente mapeado a las Entidades POCO de destino.
- Facilidad de desarrollo en SQL . Encuentro que crear consultas aplanadas
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.
- 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 back-end de StackOverflow y, bueno, Randy Burden es una superestrella. ¿Necesito decir algo más?
- Velocidad de desarrollo. Pude hacer 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 FTSE. Había tan poco código que no hubo ningún comportamiento inesperado.
Desventajas
- Se devolvió la 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 trae duplicados), en su lugar debemos usar múltiples select
declaraciones y unir todo de nuevo en el lado del cliente (vea 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 más 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 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, usando 3 lines of code to return the dynamic
.
Dapper + Slapper.Automapper
: 2.463 milisegundos por consulta, usando 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 uno 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; }
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 la TestContact
tabla (esto corresponde a la List<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 =
using (var conn = new SqlConnection(connectionString))
{
conn.Open();
{
dynamic test = conn.Query<dynamic>(sql);
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 uno TestContact
tiene un List<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 usar Attributes
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
y left 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 propiedad int 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