EF Core:asignación de herencia

EF Core:asignación de herencia

Hay dos formas de hacer un mapeo de herencia en EF Core:

  • Tabla por jerarquía (TPH) =Hay una sola tabla para todas las clases en la jerarquía.
  • Tabla por tipo (TPT) =Hay una tabla por clase en la jerarquía. Está disponible en EF Core 5 y superior.

Digamos que tenemos una base de datos con empleados. Todos los empleados tienen una identificación y un nombre. Actualmente hay dos tipos de empleados:programadores y conductores. Los programadores tienen un lenguaje (por ejemplo, C#) y los conductores tienen un automóvil (por ejemplo, Honda). Podemos modelar esto con la siguiente jerarquía de clases:

Con el mapeo TPH, tenemos una sola tabla que contiene columnas para todas las clases en jerarquía + una columna discriminadora:

Con el mapeo TPT, tenemos una tabla para cada clase en la jerarquía:

Esta diferencia en la estructura de la tabla tiene implicaciones de rendimiento y validación, que explicaré a continuación. Después de eso, mostraré cómo configurar el mapeo de TPH y TPT.

Diferencias clave entre TPH y TPT

Hay dos diferencias clave entre TPT y TPH:

1:TPH tiene un rendimiento de consulta potencialmente mejor

Con TPH, los datos están todos en una tabla. Con TPT, los datos se dividen en varias tablas, lo que requiere que realice uniones. En teoría, tener que unir varias tablas tendrá peor rendimiento que seleccionar de una sola tabla.

Cuando EF Core genera consultas para TPH, agrega la columna discriminadora en la cláusula WHERE. Si esta columna no está en un índice, tiene el potencial de degradar el rendimiento. De forma predeterminada, la columna discriminadora no se incluye en un índice. Recomendaría realizar pruebas de rendimiento para determinar si debe agregar la columna discriminadora a un índice.

2:TPT le permite hacer que las columnas de subclase sean obligatorias

Con TPT, cada subclase tiene su propia tabla, por lo que puede hacer que las columnas sean obligatorias (agregando el atributo [Requerido]). En otras palabras, puede hacer que NO sean NULOS.

Por otro lado, con TPH, todas las columnas de la subclase están en la misma tabla. Esto significa que tienen que ser anulables. Cuando inserta un registro para una subclase (por ejemplo, Programador), no tendrá un valor para las columnas que pertenecen a la otra subclase (por ejemplo, Controlador). Por lo tanto, tiene sentido que no se requieran estas columnas. Incluso si agrega el atributo [Requerido], se ignorará al generar la migración y la columna se establecerá como anulable. Si obliga a que la columna sea NOT NULL, tendrá problemas al insertar registros, así que evite hacerlo.

Configurar el mapeo de herencia

En esta sección, mostraré cómo configurar ambos tipos de asignación de herencia (TPH y TPT) para la jerarquía de clases de empleados que se muestra al principio de este artículo. Este será un ejemplo completo que muestra cómo agregar tablas, insertar datos de muestra y ejecutar consultas (para ver el SQL generado por EF Core).

Nota:la configuración se realizará a través de una clase DbContext personalizada.

Clases modelo

Primero, definamos las clases modelo para la jerarquía de empleados:

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

public abstract class EmployeeBase
{
	[Key]
	[DatabaseGenerated(DatabaseGeneratedOption.None)]
	public int Id { get; set; }
	public string Name { get; set; }
}

public class Driver : EmployeeBase
{
	public string Car { get; set; }
}

public class Programmer : EmployeeBase
{
	public string Language { get; set; }
}
Code language: C# (cs)

Estas clases de modelos serán las mismas para el mapeo de TPH y TPT.

Mapeo de TPH

1:agregue DbSet para todas las clases en la jerarquía

Agregue propiedades DbSet al contexto para todas las clases (incluida la clase base):

using Microsoft.EntityFrameworkCore;

public class CustomContext : DbContext
{
	//rest of class

	public DbSet<EmployeeBase> Employees { get; set; }
	public DbSet<Programmer> Programmers { get; set; }
	public DbSet<Driver> Drivers { get; set; }
}
Code language: C# (cs)

Como mínimo, eso es todo lo que tiene que hacer para habilitar el mapeo de TPH.

2 – Configurar discriminador

La columna del discriminador predeterminado se llama "Discriminador" y los valores del discriminador son los nombres de las subclases (Programador, Controlador).

Puede personalizar el nombre de la columna del discriminador y los valores del discriminador para cada subclase. Por ejemplo, supongamos que desea que el discriminador se llame "Tipo" y use 'P' para Programador y 'D' para Controlador. Aquí se explica cómo personalizar el discriminador:

using Microsoft.EntityFrameworkCore;

public class CustomContext : DbContext
{
	protected override void OnModelCreating(ModelBuilder modelBuilder)
	{
		modelBuilder.Entity<EmployeeBase>()
			.HasDiscriminator<char>("Type")
			.HasValue<Programmer>('P')
			.HasValue<Driver>('D');
	}

	//rest of class
}
Code language: C# (cs)

3 – Generar una migración y aplicarla

Ejecute lo siguiente para generar una migración:

dotnet ef migrations add InitTPH
Code language: PowerShell (powershell)

Esto generará el siguiente código de migración en /Migrations/ _InitTPH.cs:

protected override void Up(MigrationBuilder migrationBuilder)
{
	migrationBuilder.CreateTable(
		name: "Employees",
		columns: table => new
		{
			Id = table.Column<int>(type: "int", nullable: false),
			Name = table.Column<string>(type: "nvarchar(max)", nullable: true),
			Type = table.Column<string>(type: "nvarchar(1)", nullable: false),
			Car = table.Column<string>(type: "nvarchar(max)", nullable: true),
			Language = table.Column<string>(type: "nvarchar(max)", nullable: true)
		},
		constraints: table =>
		{
			table.PrimaryKey("PK_Employees", x => x.Id);
		});
}

Code language: C# (cs)

Ejecute lo siguiente para aplicar la migración:

dotnet ef database update
Code language: PowerShell (powershell)

Esto creará la tabla Empleados utilizando la definición de tabla que se muestra en el código de migración anterior.

4 – Insertar datos de muestra

Para ver cómo EF Core maneja las inserciones cuando usa el mapeo TPH, inserte algunos datos de muestra:

using (var context = new CustomContext(connectionString))
{
	context.Add(new Programmer()
	{
		Id = 1,
		Name = "Bob",
		Language = "C#"
	});

	context.Add(new Driver()
	{
		Id = 2,
		Name = "Alice",
		Car = "Honda"
	});

	context.SaveChanges();
}
Code language: C# (cs)

Genera las siguientes consultas de inserción para el código anterior:

exec sp_executesql N'SET NOCOUNT ON;
INSERT INTO [Employees] ([Id], [Language], [Name], [Type])
VALUES (@p0, @p1, @p2, @p3);
',N'@p0 int,@p1 nvarchar(4000),@p2 nvarchar(4000),@p3 nvarchar(1)',@p0=1,@p1=N'C#',@p2=N'Bob',@p3=N'P'

exec sp_executesql N'SET NOCOUNT ON;
INSERT INTO [Employees] ([Id], [Car], [Name], [Type])
VALUES (@p0, @p1, @p2, @p3);
',N'@p0 int,@p1 nvarchar(4000),@p2 nvarchar(4000),@p3 nvarchar(1)',@p0=2,@p1=N'Honda',@p2=N'Alice',@p3=N'D'
Code language: plaintext (plaintext)

La tabla Empleados en la base de datos se verá así:

5 – Ejecutar una consulta SELECT

Para ver lo que genera EF Core para las consultas SELECT al usar el mapeo TPH, obtenga algunos datos:

using (var context = new CustomContext(connectionString))
{
	foreach(var programmer in context.Programmers)
	{
		Console.WriteLine($"{programmer.Name} uses {programmer.Language}");
	}
}
Code language: C# (cs)

Genera la siguiente consulta SELECT:

SELECT [e].[Id], [e].[Name], [e].[Type], [e].[Language]
FROM [Employees] AS [e]
WHERE [e].[Type] = N'P'
Code language: plaintext (plaintext)

Observe que agregó WHERE Type='P' para que solo seleccione las filas del Programador.

6 – Agregar el discriminador a un índice

De forma predeterminada, la columna discriminadora no se agrega a un índice. Debido a que la columna del discriminador se agrega automáticamente a cada consulta, esto tiene el potencial de degradar el rendimiento. Asegúrese de realizar sus propias pruebas de rendimiento para determinar si esto es realmente un problema para usted.

Si decide que desea agregar la columna discriminadora, puede agregar el índice como lo haría con cualquier otra columna. Lo único especial de la columna del discriminador es que tiene un nombre predeterminado ("Discriminador"). Asegúrese de usar el nombre correcto. Este es un ejemplo de cómo agregar un índice con el nombre de columna del discriminador predeterminado:

using Microsoft.EntityFrameworkCore;

public class CustomContext : DbContext
{
	protected override void OnModelCreating(ModelBuilder modelBuilder)
	{
		modelBuilder.Entity<EmployeeBase>()
			.HasIndex("Discriminator");
	}
	
	//rest of class
}
Code language: C# (cs)

Asignación de TPT

Está disponible en EF Core 5 y superior.

1:agregue DbSet para todas las clases en la jerarquía

Agregue propiedades DbSet al contexto para todas las clases (incluida la clase base):

using Microsoft.EntityFrameworkCore;

public class CustomContext : DbContext
{
	//rest of class

	public DbSet<EmployeeBase> Employees { get; set; }
	public DbSet<Programmer> Programmers { get; set; }
	public DbSet<Driver> Drivers { get; set; }
}
Code language: C# (cs)

Nota:Este es el mismo primer paso que realiza para TPH.

2 – Asignar cada clase a una tabla

En OnModelCreating(), llame a .ToTable() para cada clase en la jerarquía de empleados:

using Microsoft.EntityFrameworkCore;


public class CustomContext : DbContext
{
	protected override void OnModelCreating(ModelBuilder modelBuilder)
	{
		modelBuilder.Entity<EmployeeBase>().ToTable("Employees");
		modelBuilder.Entity<Programmer>().ToTable("Programmers");
		modelBuilder.Entity<Driver>().ToTable("Drivers");
	}
	
	//rest of class
}
Code language: C# (cs)

Este + paso 1 es lo mínimo que debe hacer para habilitar el mapeo TPT.

3 – Generar una migración y aplicarla

Ejecute lo siguiente para generar una migración:

dotnet ef migrations add InitTPT
Code language: PowerShell (powershell)

Esto generará el siguiente código de migración en /Migrations/ _InitTPT.cs:

protected override void Up(MigrationBuilder migrationBuilder)
{
	migrationBuilder.CreateTable(
		name: "Employees",
		columns: table => new
		{
			Id = table.Column<int>(type: "int", nullable: false),
			Name = table.Column<string>(type: "nvarchar(max)", nullable: true)
		},
		constraints: table =>
		{
			table.PrimaryKey("PK_Employees", x => x.Id);
		});

	migrationBuilder.CreateTable(
		name: "Drivers",
		columns: table => new
		{
			Id = table.Column<int>(type: "int", nullable: false),
			Car = table.Column<string>(type: "nvarchar(max)", nullable: true)
		},
		constraints: table =>
		{
			table.PrimaryKey("PK_Drivers", x => x.Id);
			table.ForeignKey(
				name: "FK_Drivers_Employees_Id",
				column: x => x.Id,
				principalTable: "Employees",
				principalColumn: "Id",
				onDelete: ReferentialAction.Restrict);
		});

	migrationBuilder.CreateTable(
		name: "Programmers",
		columns: table => new
		{
			Id = table.Column<int>(type: "int", nullable: false),
			Language = table.Column<string>(type: "nvarchar(max)", nullable: true)
		},
		constraints: table =>
		{
			table.PrimaryKey("PK_Programmers", x => x.Id);
			table.ForeignKey(
				name: "FK_Programmers_Employees_Id",
				column: x => x.Id,
				principalTable: "Employees",
				principalColumn: "Id",
				onDelete: ReferentialAction.Restrict);
		});
}
Code language: C# (cs)

Ejecute lo siguiente para aplicar la migración:

dotnet ef database update
Code language: PowerShell (powershell)

Esto creará las tablas de Empleados, Programadores y Conductores. Vinculará las tablas de programadores/controladores a la tabla de empleados con una clave externa (id).

4 – Insertar datos de muestra

Para ver cómo EF Core maneja las inserciones cuando usa el mapeo TPT, inserte algunos datos de muestra:

using (var context = new CustomContext(connectionString))
{
	context.Add(new Programmer()
	{
		Id = 1,
		Name = "Jane",
		Language = "Java"
	});

	context.Add(new Driver()
	{
		Id = 2,
		Name = "Frank",
		Car = "Ford"
	});

	context.SaveChanges();
}
Code language: C# (cs)

Genera las siguientes consultas de inserción para el código anterior:

exec sp_executesql N'SET NOCOUNT ON;
INSERT INTO [Employees] ([Id], [Name])
VALUES (@p0, @p1);
',N'@p0 int,@p1 nvarchar(4000)',@p0=1,@p1=N'Jane'

exec sp_executesql N'SET NOCOUNT ON;
INSERT INTO [Employees] ([Id], [Name])
VALUES (@p0, @p1);
',N'@p0 int,@p1 nvarchar(4000)',@p0=2,@p1=N'Frank'

exec sp_executesql N'SET NOCOUNT ON;
INSERT INTO [Drivers] ([Id], [Car])
VALUES (@p0, @p1);
',N'@p0 int,@p1 nvarchar(4000)',@p0=2,@p1=N'Ford'

exec sp_executesql N'SET NOCOUNT ON;
INSERT INTO [Programmers] ([Id], [Language])
VALUES (@p0, @p1);
',N'@p0 int,@p1 nvarchar(4000)',@p0=1,@p1=N'Java'
Code language: plaintext (plaintext)

Las tres tablas en la base de datos se verán así:

5 – Ejecutar una consulta SELECT

Veamos qué consulta SQL genera EF Core al seleccionar datos:

using (var context = new CustomContext(connectionString))
{
	foreach (var driver in context.Drivers)
	{
		Console.WriteLine($"{driver.Name} drives {driver.Car}");
	}
} 
Code language: C# (cs)

Genera la siguiente consulta con una unión:

SELECT [e].[Id], [e].[Name], [d].[Car]
FROM [Employees] AS [e]
INNER JOIN [Drivers] AS [d] ON [e].[Id] = [d].[Id]
Code language: plaintext (plaintext)

Siempre tiene que unirse a las tablas para obtener los registros completos del controlador/programador.