0

[Article] Novedades! Entity Framework Core 5 Preview 8

En este ultimo post, veremos todas las novedades de Entity Framework Core 5 Preview 8. Veamos que tenemos para esta versión!.

Entity Framework

Para incluir esta preview en nuestros proyectos podemos ejecutar el siguiente comando desde la consola:

dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 5.0.0-preview.8.20407.4

Instalación de la interfaz de línea de comandos (CLI) de EF Core

Como hace un tiempo, el cliente de entity framework  no se incluye en el SDK. Para poder ejecutar comandos de migration o scaffolding  deberemos instalar el paquete como una herramienta global o local.

Los comandos:

  • dotnet tool install –global dotnet-ef –version 5.0.0-preview.8.20407.4
  • dotnet tool update –global dotnet-ef –version 5.0.0-preview.8.20407.4

Novedades

Asignación de tabla por tipo (TPT)

De forma predeterminada, EF Core asigna una jerarquía de herencia de tipos .NET a una única tabla de base de datos. Es conocida como asignación de tabla por jerarquía (TPH). EF Core 5.0 también permite asignar cada tipo de .NET en una jerarquía de herencia a una tabla de base de datos diferente conocida como asignación de tabla por tipo (TPT).

Por ejemplo, veamos este modelo con una jerarquía asignada:

public class Animal
{
    public int Id { get; set; }
    public string Species { get; set; }
}

public class Pet : Animal
{
    public string Name { get; set; }
}

public class Cat : Pet
{
    public string EdcuationLevel { get; set; }
}

public class Dog : Pet
{
    public string FavoriteToy { get; set; }
}

Como se mapea una tabla simple:

CREATE TABLE [Animals] (
    [Id] int NOT NULL IDENTITY,
    [Species] nvarchar(max) NULL,
    [Discriminator] nvarchar(max) NOT NULL,
    [Name] nvarchar(max) NULL,
    [EdcuationLevel] nvarchar(max) NULL,
    [FavoriteToy] nvarchar(max) NULL,
    CONSTRAINT [PK_Animals] PRIMARY KEY ([Id])
);

Pero, la asignación de cada tipo de entidad a una tabla diferente dará como resultado una tabla por tipo:

CREATE TABLE [Animals] (
    [Id] int NOT NULL IDENTITY,
    [Species] nvarchar(max) NULL,
    CONSTRAINT [PK_Animals] PRIMARY KEY ([Id])
);

CREATE TABLE [Pets] (
    [Id] int NOT NULL,
    [Name] nvarchar(max) NULL,
    CONSTRAINT [PK_Pets] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Pets_Animals_Id] FOREIGN KEY ([Id]) REFERENCES [Animals] ([Id]) ON DELETE NO ACTION
);

CREATE TABLE [Cats] (
    [Id] int NOT NULL,
    [EdcuationLevel] nvarchar(max) NULL,
    CONSTRAINT [PK_Cats] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Cats_Animals_Id] FOREIGN KEY ([Id]) REFERENCES [Animals] ([Id]) ON DELETE NO ACTION,
    CONSTRAINT [FK_Cats_Pets_Id] FOREIGN KEY ([Id]) REFERENCES [Pets] ([Id]) ON DELETE NO ACTION
);

CREATE TABLE [Dogs] (
    [Id] int NOT NULL,
    [FavoriteToy] nvarchar(max) NULL,
    CONSTRAINT [PK_Dogs] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Dogs_Animals_Id] FOREIGN KEY ([Id]) REFERENCES [Animals] ([Id]) ON DELETE NO ACTION,
    CONSTRAINT [FK_Dogs_Pets_Id] FOREIGN KEY ([Id]) REFERENCES [Pets] ([Id]) ON DELETE NO ACTION
);

Los tipos de entidad se pueden asignar a diferentes tablas mediante atributos de asignación:

Table("Animals")]
public class Animal
{
    public int Id { get; set; }
    public string Species { get; set; }
}

[Table("Pets")]
public class Pet : Animal
{
    public string Name { get; set; }
}

[Table("Cats")]
public class Cat : Pet
{
    public string EdcuationLevel { get; set; }
}

[Table("Dogs")]
public class Dog : Pet
{
    public string FavoriteToy { get; set; }
}

Por ultimo, Model Builder:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Animal>().ToTable("Animals");
    modelBuilder.Entity<Pet>().ToTable("Pets");
    modelBuilder.Entity<Cat>().ToTable("Cats");
    modelBuilder.Entity<Dog>().ToTable("Dogs");
}

Migration: Reconstruir tablas SQLite

En SQLite tenemos algunas limitaciones de funcionalidades, por ejemplo si queres quitar una columna a una tabla, será necesario eliminar la tabla y volverla a crear. En esta preview ahora es posible la reconstrucción automática de la tabla cuando necesitamos cambiar el esquema de la misma.

public class Unicorn
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
}
CREATE TABLE "Unicorns" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Unicorns" PRIMARY KEY AUTOINCREMENT,
    "Name" TEXT NULL,
    "Age" INTEGER NOT NULL
);

Consideremos quitar la propiedad Age propiedad, agregar una nueva migración y actualizar la base de datos. Se producirá un error en la actualización al usar EF Core 3.1 porque no se puede quitar la columna. En EF Core 5.0, las migraciones volverán a generar la tabla:

CREATE TABLE "ef_temp_Unicorns" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Unicorns" PRIMARY KEY AUTOINCREMENT,
    "Name" TEXT NULL
);

INSERT INTO "ef_temp_Unicorns" ("Id", "Name")
SELECT "Id", "Name"
FROM Unicorns;

PRAGMA foreign_keys = 0;

DROP TABLE "Unicorns";

ALTER TABLE "ef_temp_Unicorns" RENAME TO "Unicorns";

PRAGMA foreign_keys = 1;

Lo que sucederá:

  • Se crea una tabla temporal con el esquema deseado para la nueva tabla
  • Los datos se copian de la tabla actual en la tabla temporal
  • La aplicación de claves externas está desactivada
  • Se descarta la tabla actual
  • Se cambia el nombre de la tabla temporal para que sea la nueva tabla

Funciones con valores de tabla

Se incluye compatibilidad de primera clase para asignar métodos .NET a funciones con valores de tabla (TVF). Estas funciones podemos usarlas en consultas LINQ donde la composición adicional en los resultados de la función también se traducirá a SQL.

Ejemplo:

create FUNCTION GetReports(@employeeId int)
RETURNS @reports TABLE
(
    Name nvarchar(50) not null,
    IsDeveloper bit not null
)
AS
begin
    WITH cteEmployees AS
    (
        SELECT id, name, managerId, isDeveloper
        FROM employees
        WHERE id = @employeeId
        UNION ALL
        SELECT e.id, e.name, e.managerId, e.isDeveloper
        FROM employees e
        INNER JOIN cteEmployees cteEmp ON cteEmp.id = e.ManagerId
    )

    insert into @reports
    select name, isDeveloper
    FROM cteEmployees
    where id != @employeeId

    return
end

El modelo de EF Core requiere dos tipos de entidad para usar este TVF: Un Employee que se asigna a la tabla Empleados de la manera normal y un Report que coincide con la forma devuelta por la TVF.

public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
    public bool IsDeveloper { get; set; }

    public int? ManagerId { get; set; }
    public virtual Employee Manager { get; set; }
}

public class Report
{
    public string Name { get; set; }
    public bool IsDeveloper { get; set; }
}

Estos tipos deben incluirse en el modelo EF Core:

modelBuilder.Entity<Employee>();
modelBuilder.Entity(typeof(Report)).HasNoKey();

Tengamos en cuenta que Report no tiene clave principal, por lo tanto, debe configurarse como tal.

Por último, debemos asignar un método .NET al TVF en la base de datos. Este método se puede definir en DbContext utilizando el nuevo método FromExpression:

public IQueryable<Report> GetReports(int managerId)
    => FromExpression(() => GetReports(managerId));

Este método utiliza un parámetro y un tipo de retorno que coinciden con el TVF definido anteriormente. Luego, el método se agrega al modelo EF Core en OnModelCreating:

modelBuilder.HasDbFunction(() => GetReports(0));

Ahora podemos escribir consultas que llamen a GetReports y redactar sobre los resultados. Por ejemplo:

from e in context.Employees
from rc in context.GetReports(e.Id)
where rc.IsDeveloper == true
select new
{
  ManagerName = e.Name,
  EmployeeName = rc.Name,
})

El resultado en SQL:

SELECT [e].[Name] AS [ManagerName], [g].[Name] AS [EmployeeName]
FROM [Employees] AS [e]
CROSS APPLY [dbo].[GetReports]([e].[Id]) AS [g]
WHERE [g].[IsDeveloper] = CAST(1 AS bit)

Asignación flexible de consultas/actualizaciones

EF Core 5.0 permite asignar el mismo tipo de entidad a diferentes objetos de base de datos. Estos objetos pueden ser tablas, vistas o funciones. Por ejemplo, un tipo de entidad se puede asignar a una vista de base de datos y a una tabla de base de datos:

Por ejemplo, un tipo de entidad se puede asignar a una vista de base de datos y a una tabla de base de datos:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<Blog>()
        .ToTable("Blogs")
        .ToView("BlogsView");
}

De forma predeterminada, EF Core consultará desde la vista y enviará actualizaciones a la tabla. Por ejemplo, ejecutando el siguiente código:

var blog = context.Set<Blog>().Single(e => e.Name == "One Unicorn");

blog.Name = "1unicorn2";

context.SaveChanges();

Resultado en el SQL:

SELECT TOP(2) [b].[Id], [b].[Name], [b].[Url]
FROM [BlogsView] AS [b]
WHERE [b].[Name] = N'One Unicorn'

SET NOCOUNT ON;
UPDATE [Blogs] SET [Name] = @p0
WHERE [Id] = @p1;
SELECT @@ROWCOUNT;

Configuración de consulta dividida en todo el contexto

Ahora es posible configurar como predeterminadas la utilización de consultas divididas para cualquier consulta ejecutada por DbContext. Esta configuración solo está disponible para proveedores relacionales, por lo que debe especificarse como parte de la configuración UseProvider.  Por ejemplo:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .UseSqlServer(
            Your.SqlServerConnectionString,
            b => b.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery));

Asignación de PhysicalAddress

La clase PhysicalAddress estándar de .NET ahora se asigna automáticamente a una columna de cadena para bases de datos que aún no tienen compatibilidad nativa

Conclusión

En este tercer post completamos todas las novedades más que interesantes en esta Preview 8. Los invito a probar estas características y funcionalidades para calmar las ansiedades de la RC!. No olviden de dar feedback al equipo que será de mucha ayuda. Nos vemos en próximos post.

Referencias

Fernando Sonego

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *