Entity Framework (EF) es el ORM oficial de Microsoft, creado, desarrollado y mantenido por este mismo. Cabe destacar que Entity Framework es Open Source, con código accesible para todo el mundo y la posibilidad de contribuir a la mejora del mismo.
El versionado de EF siempre ha sido un poco caótico, en 2010 se paso de la versión 1.0 a la 4.0 y cada cierto tiempo había saltos extraños, por ejemplo de la version 4.3.1 se paso a la versión 5.0, etc. Y a todo esto debemos sumar que con la llegada de .NET Standard llegó Entity Framework Core, que no debemos confundir con EF. De aquí en adelante nos vamos a centrar exclusivamente en EFCore.
El equipo de Entity Framework Core se alineó con .NET para acabar con los saltos de versiones anteriores y con la salida de net6.0, también llegó una nueva versión de EFCore.
@JulieLerman
En este artículo vamos a ver un poco en detalle los principales cambios que se han incluido en esta versión, y echaremos un vistazo rápido al resto de nuevas funcionalidades que se han integrado dentro de esta nueva versión.
Cambios relevantes
Soporte de tablas temporales en SQL Server
La nueva versión de EFCore, admite la creación de tablas temporales a través de migraciones, transformación de tablas existentes en tablas temporales, consulta de datos históricos y restauración de datos desde un punto pasado.
Para la configuración de tablas temporales a traves de Fluent API, podemos usar la configuración siguiente:
[scss]modelBuilder
.Entity<Student>()
.ToTable(«Students», s => s.IsTemporal());
[/scss]
Podemos observar que SQL Server creará dos columnas ocultas de tipo datetime2. PeriodEnd y PeriodStart, y pueden ser usadas junto con la tabla histórica StudentHistory para ejecutar consultas de manera normal.
Un ejemplo sería:
[scss]var students = context.Students.ToList();
foreach (var student in students)
{
var studentEntry = context.Entry(student);
var validFrom = studentEntry.Property<DateTime>(«PeriodStart»).CurrentValue;
var validTo = studentEntry.Property<DateTime>(«PeriodEnd»).CurrentValue;
Console.WriteLine($» student {student.Name} valid from {validFrom} to {validTo}»);
}
[/scss]
Starting data:
Student Pinky Pie valid from 8/26/2021 4:38:58 PM to 12/31/9999 11:59:59 PM
Student Rainbow Dash valid from 8/26/2021 4:38:58 PM to 12/31/9999 11:59:59 PM
Student Fluttershy valid from 8/26/2021 4:38:58 PM to 12/31/9999 11:59:59 PM
Otra de las características de las tablas temporales, es poder realizar la restauración de datos históricos. Por ejemplo si eliminamos un estudiante, podemos ir a consultar el registro antes de esa eliminación( a fecha timeStamp), recuperarlo y volverlo a insertar, si por ejemplo hubiera habido una equivocación en la eliminación del registro.
[scss]var student = context
.Students
.TemporalAsOf(timeStamp)
.Single(s => s.Name == «Pinky Pie»);
context.Add(student);
context.SaveChanges();
[/scss]
Modelos compilados
EFCore se caracteriza por la mejora de performance, y para ello han creado los modelos compilados, que nos dan una mejor experiencia de arranque de aplicación o de DbContext para modelos relativamente grandes(de cientos a miles de entidades-relaciones).
Los modelos compilados se crean con la herramienta de línea de comandos dotnet ef, de la siguiente manera:
[scss]dotnet ef dbcontext optimize
[/scss]
Las opciones –output-dir y –namespace se pueden usar para especificar el directorio y el espacio de nombres en el que se generará el modelo compilado.
Por ejemplo:
[scss]PS C:\dotnet\efdocs\samples\core\Miscellaneous\CompiledModels> dotnet ef dbcontext
optimize –output-dir MyCompiledModels –namespace MyCompiledModels
Build started…
Build succeeded.
Successfully generated a compiled model, to use it call
‘options.UseModel(MyCompiledModels.BlogsContextModel.Instance)’. Run this command
again when the model is modified.
PS C:\dotnet\efdocs\samples\core\Miscellaneous\CompiledModels>
[/scss]
Si consideramos el tiempo de inicio como el tiempo para realizar la primera operación en un objeto
DbContext, ya sea una lectura o una escritura, porque hay que tener en cuenta que la creación de DbContext, no hace que el modelo del mismo se inicialice. Gracias a los modelos compilados, se puede observar la siguiente mejora en este tiempo de inicialización.
@Microsoft
Tenemos que tener en cuenta que los modelos compilados tienen ciertas limitaciones, por tanto aunque la mejoría de performance en ciertos casos puede ser considerable, hay que tener en cuenta los casos en los que estamos limitados y no podremos hacer uso de esta característica.
- No se admiten filtros de consulta globales.
- No se admiten la carga diferida ni los servidores proxy de seguimiento de cambios.
- No se admiten implementaciones personalizadas de IModelCacheKeyFactory. Pero puede compilar varios modelos y cargar el adecuado según sea necesario.
- El modelo se debe sincronizar manualmente y regenerarlo cada vez que cambie su definición o configuración.
Agrupación de migraciones
Las migraciones de EFCore se usan para generar actualizaciones de esquema de base de datos basadas en cambios en el modelo de EF. Estas actualizaciones de esquema se deben aplicar en el momento de la implementación de la aplicación, a menudo como parte de un sistema de integración continua o implementación continua (C.I. o C.D.) system.
Ahora en EFCore se incluye una nueva manera de aplicar estas actualizaciones de esquema: agrupaciones de migración. Una agrupación de migración es un pequeño ejecutable que contiene migraciones y el código necesario para aplicarlas a la base de datos.
Las agrupaciones de migración se crean con la herramienta de línea de comandos dotnet ef. Una agrupación necesita que se incluyan migraciones. Se crean mediante dotnet ef migrations add como se describe en la dotnet ef migrations add. Una vez que tenga las migraciones listas para implementarse, cree una agrupación mediante dotnet ef migrations bundle. Por ejemplo:
PS C:\local\AllTogetherNow\SixOh> dotnet ef migrations bundle Build started…
Build succeeded.
Building bundle…
Done. Migrations Bundle: C:\local\AllTogetherNow\SixOh\efbundle.exe PS C:\local\AllTogetherNow\SixOh>
La salida es un ejecutable adecuado para el sistema operativo de destino. En este caso, es Windows x64, por lo que aparece una instancia de efbundle.exe en la carpeta local. Al ejecutar este ejecutable se aplican las migraciones que contiene.
Definición de propiedades globales por tipo
En las versiones anteriores de EFCore, era necesario que la asignación para cada propiedad de un tipo determinado, se configurará explícitamente si la asignación difería del valor predeterminado de la misma, por ejemplo la longitud maxima de los strings, la precision decimal, así como la conversión de valores para el tipo de la propiedad.
Y se realizaba de la siguiente manera:
Configuración del generador de modelos para cada propiedad
Un atributo de asignación en cada propiedad
Iteración explícita por todas las propiedades de todos los tipos de entidad y uso de las API de metadatos de bajo nivel al compilar el modelo.
EFCore 6 permite la configuración una única vez por tipo. Después esta se aplicará a todas las propiedades de ese tipo en el modelo. Esto se denomina «configuración del modelo anterior a la convención», ya que configura aspectos del modelo que serán usados para la creación de estos. Esta configuración se aplica el invalidar ConfigureConventions en DbContext:
[scss]public class MyDbContext : DbContext
{
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
[/scss]
Mejoras de consultas LINQ
– Compatibilidad mejorada con GroupBy
Mejoras de compatibilidad con las consultas GroupBy:
-
- Traduce GroupBy seguido de FirstOrDefault (o similar) en un grupo.
- Admite la selección de los principales N resultados de un grupo.
- Expande las navegaciones después de que se haya aplicado el operador GroupBy.
– Traducción de String.Concat con varios argumentos
A partir de EFCore 6, las llamadas a String.Concat con varios argumentos se traducen a SQL. Por ejemplo, la consulta siguiente:
[scss]var shards = context.Shards.Where(e => string.Concat(e.Token1, e.Token2, e.Token3) != e.TokensProcessed).ToList();
[/scss]
Se traducirá al siguiente código SQL al usar SQL Server:
[scss]SELECT [s].[Id], [s].[Token1], [s].[Token2], [s].[Token3], [s].[TokensProcessed] FROM [Shards] AS [s]
WHERE (([s].[Token1] + ([s].[Token2] + [s].[Token3])) <> [s].[TokensProcessed]) OR [s].[TokensProcessed] IS NULL
[/scss]
– Búsqueda de texto de SQL Server más flexible
Para EFCore 6 se han flexibilizado los parámetros para FreeText(DbFunctions, String, String) y Contains. Esto permite usar estas funciones con columnas que no sean de tipos primitivos. Por ejemplo, pongamos por caso un tipo de entidad con una propiedad Name definida como objeto de valor:
[scss]public class Customer
{
public int Id { get; set; }
public Name Name{ get; set; }
}
public class Name
{
public string First { get; set; }
public string MiddleInitial { get; set; }
public string Last { get; set; }
}
[/scss]
Se asigna a un elemento JSON en la base de datos:
modelBuilder.Entity<Customer>()
.Property(e => e.Name)
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
v => JsonSerializer.Deserialize<Name>(v, (JsonSerializerOptions)null));
Ahora se puede ejecutar una consulta con Contains o FreeText, aunque el tipo de la propiedad sea Name, no string. Por ejemplo:
[scss]var result = context.Customers.Where(e => EF.Functions.Contains(e.Name, «Martin»)).ToList();
[/scss]
Al usar SQL Server, esto genera el código SQL siguiente:
[scss]SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE CONTAINS([c].[Name], N’Martin’)
[/scss]
– EF.Functions.Random
EF.Functions.Random se asigna a una función de base de datos que devuelve un número pseudoaleatorio entre 0 y 1, ambos no incluidos. Las traducciones se han implementado en el repositorio de EFCore para SQL Server, SQLite y CosmosDB.
– Mejora de la traducción de SQL Server para IsNullOrWhitespace
Considere la consulta siguiente:
[scss]var users = context.Users.Where(
e => string.IsNullOrWhiteSpace(e.FirstName)
|| string.IsNullOrWhiteSpace(e.LastName)).ToList();
[/scss]
Antes de EFCore 6, esto se traducía a lo siguiente en SQL Server:
[scss]SELECT [u].[Id], [u].[FirstName], [u].[LastName]
FROM [Users] AS [u]
WHERE ([u].[FirstName] IS NULL OR (LTRIM(RTRIM([u].[FirstName])) = N»)) OR ([u].[LastName] IS NULL OR (LTRIM(RTRIM([u].[LastName])) = N»))
[/scss]
Esta traducción se ha mejorado para EFCore 6 a:
[scss]SELECT [u].[Id], [u].[FirstName], [u].[LastName]
FROM [Users] AS [u]
WHERE ([u].[FirstName] IS NULL OR ([u].[FirstName] = N»)) OR ([u].[LastName] IS NULL OR ([u].[LastName] = N»))
[/scss]
– Definición de una consulta para el proveedor en memoria
Se puede usar un nuevo método ToInMemoryQuery para escribir una consulta de definición en la base de datos en memoria para un tipo de entidad determinado. Esto es muy útil para crear el equivalente de las vistas en la base de datos en memoria, especialmente cuando dichas vistas devuelven tipos de entidad sin clave. Por ejemplo, considere una base de datos de clientes. Cada cliente tiene una dirección:
[scss]public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public Address Address { get; set; }
}
public class Address
{
public int Id { get; set; }
public string House { get; set; } public string Street { get; set; }
public string City { get; set; }
public string Postcode { get; set; }
}
[/scss]
Ahora, queremos una vista de estos datos que muestre cuántos clientes hay en cada área de código postal. Se puede crear un tipo de entidad sin clave para representarlo:
[scss]public class CustomerDensity
{
public string Postcode { get; set; }
public int CustomerCount { get; set; }
}
[/scss]
Y definir una propiedad DbSet para ella en DbContext, junto con conjuntos para otros tipos de entidad de nivel superior:
[scss]public DbSet<Customer> Customers { get; set; }
public DbSet<CustomerDensity> CustomerDensities { get; set; }
[/scss]
Después, en OnModelCreating, se puede escribir una consulta LINQ que defina los datos que se devolverán para CustomerDensities:
[scss]protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<CustomerDensity>() .HasNoKey()
.ToInMemoryQuery(
() => Customers
.GroupBy(c => c.Address.Postcode.Substring(0, 3))
.Select(
g =>
new CustomerDensity
{
Postcode = g.Key,
CustomerCount = g.Count()
}));
}
[/scss]
Y así se puede consultar como cualquier otra propiedad DbSet:
[scss]var results = context.CustomerDensities.ToList();
[/scss]
– División de consultas para colecciones que no son de navegación
EFCore permite dividir una sola consulta LINQ en varias consultas SQL. En EFCore 6, se ha ampliado esta compatibilidad para incluir casos en los que las colecciones que no son de navegación estén contenidas en la proyección de consulta.
– Se ha quitado la última cláusula ORDER BY al realizar la unión en colecciones
Al cargar entidades uno a varios relacionadas, EFCore agrega cláusulas ORDER BY para asegurarse de que todas las entidades relacionadas de una entidad determinada se agrupen juntas. Pero la última cláusula ORDER BY no es necesaria para que EF genere las agrupaciones necesarias y puede afectar al rendimiento. Por lo tanto, en EFCore 6 se ha quitado esta cláusula.
– Etiquetado de consultas con el nombre de archivo y el número de línea
Las etiquetas de consulta permiten agregar un tag a una consulta LINQ de modo que se incluya en el archivo SQL generado. En EFCore 6, se puede usar para etiquetar las consultas con el nombre de archivo y el número de línea del código LINQ.
– Cambios en el control dependiente opcional de propiedad
Resulta complicado saber si existe o no una entidad dependiente opcional cuando comparte una tabla con su entidad principal. Esto se debe a que hay una fila en la tabla para la entidad dependiente porque la entidad de seguridad la necesita, independientemente de si la dependiente existe o no. La manera de controlar esto de forma inequívoca es asegurarse de que la entidad dependiente tiene al menos una propiedad necesaria. Puesto que una propiedad obligatoria no puede ser NULL, significa que si el valor de la columna de esa propiedad es NULL, la entidad dependiente no existe.
Rendimiento
Se han realizado varias mejoras que hacen un cambio significativo en la performance de las consultas de EFCore respecto a otras versiones.
- Ahora el rendimiento de EFCore 6 es un 70% más rápido según el estándar de referencia TechEmpower Fortunes, en comparación con la versión 5.0.
- Esta es la mejora de rendimiento full-stack, incluidas las mejoras en el código de benchmark core, el entorno de ejecución de .NET, etc.
- El propio EFCore 6 es un 31% más rápido al ejecutar consultas sin seguimiento.
- Las asignaciones de pila se han reducido un 43% al ejecutar consultas.
Después de estas mejoras, el uso de Dapper (micro-ORM) puede llegar a ser discutible, ya que según TechEmpower Fortunes, la diferencia entre EFCore6 y Dapper se ha recortado de un 55% en versiones anteriores a un 5% en la actual versión, lo que haría que en ciertas ocasiones no mereciera la pena el uso de introducir un componente más en nuestro proyecto.
Este y los otros puntos de performance, los estudiaremos más en detalle en siguientes artículos.
Breaking Changes
Cabe destacar que también hay cambios importantes que deberían ser revisados por los desarrolladores, a continuación, se enumeran los breaking changes de la versión 6.0 de EFCore.