Asignación avanzada de tablas

EF Core ofrece mucha flexibilidad cuando se trata de asignar tipos de entidad a tablas de una base de datos. Esto resulta aún más útil cuando necesita usar una base de datos que no ha sido creada por EF.

Las técnicas siguientes se describen en términos de tablas, pero también se puede lograr el mismo resultado al asignar a vistas.

División de tablas

EF Core permite asignar dos o más entidades a una sola fila. Esto es llamada división de tablas o uso compartido de tablas.

Configuración

Para usar la división en tablas, los tipos de entidad deben estar asignados a la misma tabla, tener las claves primarias asignadas a las mismas columnas y al menos una relación configurada entre la clave primaria de un tipo de entidad y otra en la misma tabla.

Un escenario frecuente para la división de tablas es el uso de solo un subconjunto de columnas de la tabla para obtener un mayor rendimiento o encapsulación.

En este ejemplo Order se representa un subconjunto de DetailedOrder.

public class Order
{
    public int Id { get; set; }
    public OrderStatus? Status { get; set; }
    public DetailedOrder DetailedOrder { get; set; }
}
public class DetailedOrder
{
    public int Id { get; set; }
    public OrderStatus? Status { get; set; }
    public string BillingAddress { get; set; }
    public string ShippingAddress { get; set; }
    public byte[] Version { get; set; }
}

Además de la configuración necesaria, llamamos Property(o => o.Status).HasColumnName("Status") a para asignar DetailedOrder.Status a la misma columna queOrder.Status.

modelBuilder.Entity<DetailedOrder>(
    dob =>
    {
        dob.ToTable("Orders");
        dob.Property(o => o.Status).HasColumnName("Status");
    });

modelBuilder.Entity<Order>(
    ob =>
    {
        ob.ToTable("Orders");
        ob.Property(o => o.Status).HasColumnName("Status");
        ob.HasOne(o => o.DetailedOrder).WithOne()
            .HasForeignKey<DetailedOrder>(o => o.Id);
        ob.Navigation(o => o.DetailedOrder).IsRequired();
    });

Sugerencia

Vea el proyecto de ejemplo completo para obtener más contexto.

Uso

Guardar y consultar entidades mediante la división de tablas se realiza de la misma manera que otras entidades:

using (var context = new TableSplittingContext())
{
    context.Database.EnsureDeleted();
    context.Database.EnsureCreated();

    context.Add(
        new Order
        {
            Status = OrderStatus.Pending,
            DetailedOrder = new DetailedOrder
            {
                Status = OrderStatus.Pending,
                ShippingAddress = "221 B Baker St, London",
                BillingAddress = "11 Wall Street, New York"
            }
        });

    context.SaveChanges();
}

using (var context = new TableSplittingContext())
{
    var pendingCount = context.Orders.Count(o => o.Status == OrderStatus.Pending);
    Console.WriteLine($"Current number of pending orders: {pendingCount}");
}

using (var context = new TableSplittingContext())
{
    var order = context.DetailedOrders.First(o => o.Status == OrderStatus.Pending);
    Console.WriteLine($"First pending order will ship to: {order.ShippingAddress}");
}

Entidad dependiente opcional

Si todas las columnas usadas por una entidad dependiente están NULL en la base de datos, no se creará ninguna instancia para ella cuando se consulte. Esto permite modelar una entidad dependiente opcional, donde la propiedad de relación en la entidad de seguridad sería null. Tenga en cuenta que esto también ocurriría si todas las propiedades de los dependientes son opcionales y se establecen en null, lo que podría no ser esperado.

Sin embargo, la comprobación adicional puede afectar al rendimiento de las consultas. Además, si el tipo de entidad dependiente tiene dependientes propios, determinar si se debe crear una instancia se convierte en no trivial. Para evitar estos incidentes, el tipo de entidad dependiente se puede marcar como obligatorio, consulte Requerido dependientes uno a uno para obtener más información.

Tokens de simultaneidad

Si alguno de los tipos de entidad que comparten una tabla tiene un token de simultaneidad, también debe incluirse en todos los demás tipos de entidad. Esto es necesario que sea en orden para evitar un valor de token de simultaneidad obsoleto cuando solo se actualiza una de las entidades asignadas a la misma tabla.

Para evitar exponer el token de simultaneidad al código de consumo, es posible crear uno como propiedad shadow :

modelBuilder.Entity<Order>()
    .Property<byte[]>("Version").IsRowVersion().HasColumnName("Version");

modelBuilder.Entity<DetailedOrder>()
    .Property(o => o.Version).IsRowVersion().HasColumnName("Version");

Herencia

Se recomienda leer la página dedicada en la herencia antes de continuar con esta sección.

Los tipos dependientes que usan la división de tablas pueden tener una jerarquía de herencia, pero hay algunas limitaciones:

  • El tipo de entidad dependiente no puede usar la asignación de TPC, ya que los tipos derivados no pueden asignarse a la misma tabla.
  • El tipo de entidad dependiente puede usar la asignación de TPT, pero solo el tipo de entidad raíz puede usar la división de tablas.
  • Si el tipo de entidad principal usa TPC, solo los tipos de entidad que no tienen descendientes pueden usar la división de tablas. De lo contrario, las columnas dependientes deben duplicarse en las tablas correspondientes a los tipos derivados, complicando todas las interacciones.

División de entidades

EF Core permite asignar una entidad a filas en dos o más tablas. Esto se denomina división de entidades.

Configuración

Por ejemplo, considere una base de datos con tres tablas que contienen datos del cliente:

  • Una tabla Customers para obtener información del cliente
  • Una tabla PhoneNumbers para el número de teléfono del cliente
  • Una tablaAddresses para la dirección del cliente

Estas son las definiciones de estas tablas en SQL Server:

CREATE TABLE [Customers] (
    [Id] int NOT NULL IDENTITY,
    [Name] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Customers] PRIMARY KEY ([Id])
);
    
CREATE TABLE [PhoneNumbers] (
    [CustomerId] int NOT NULL,
    [PhoneNumber] nvarchar(max) NULL,
    CONSTRAINT [PK_PhoneNumbers] PRIMARY KEY ([CustomerId]),
    CONSTRAINT [FK_PhoneNumbers_Customers_CustomerId] FOREIGN KEY ([CustomerId]) REFERENCES [Customers] ([Id]) ON DELETE CASCADE
);

CREATE TABLE [Addresses] (
    [CustomerId] int NOT NULL,
    [Street] nvarchar(max) NOT NULL,
    [City] nvarchar(max) NOT NULL,
    [PostCode] nvarchar(max) NULL,
    [Country] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Addresses] PRIMARY KEY ([CustomerId]),
    CONSTRAINT [FK_Addresses_Customers_CustomerId] FOREIGN KEY ([CustomerId]) REFERENCES [Customers] ([Id]) ON DELETE CASCADE
);

Cada una de estas tablas normalmente se asignaría a su propio tipo de entidad, con relaciones entre los tipos. Sin embargo, si las tres tablas siempre se usan juntas, puede ser más conveniente asignarlas a un solo tipo de entidad. Por ejemplo:

public class Customer
{
    public Customer(string name, string street, string city, string? postCode, string country)
    {
        Name = name;
        Street = street;
        City = city;
        PostCode = postCode;
        Country = country;
    }

    public int Id { get; set; }
    public string Name { get; set; }
    public string? PhoneNumber { get; set; }
    public string Street { get; set; }
    public string City { get; set; }
    public string? PostCode { get; set; }
    public string Country { get; set; }
}

Esto se logra en EF7 llamando SplitToTable a para cada división en el tipo de entidad. Por ejemplo, el código siguiente divide el tipo de entidad Customer en las tablas Customers, PhoneNumbers, y Addresses mostradas anteriormente:

modelBuilder.Entity<Customer>(
    entityBuilder =>
    {
        entityBuilder
            .ToTable("Customers")
            .SplitToTable(
                "PhoneNumbers",
                tableBuilder =>
                {
                    tableBuilder.Property(customer => customer.Id).HasColumnName("CustomerId");
                    tableBuilder.Property(customer => customer.PhoneNumber);
                })
            .SplitToTable(
                "Addresses",
                tableBuilder =>
                {
                    tableBuilder.Property(customer => customer.Id).HasColumnName("CustomerId");
                    tableBuilder.Property(customer => customer.Street);
                    tableBuilder.Property(customer => customer.City);
                    tableBuilder.Property(customer => customer.PostCode);
                    tableBuilder.Property(customer => customer.Country);
                });
    });

Observe también que, si es necesario, se pueden especificar nombres de columna diferentes para cada una de las tablas. Para configurar el nombre de columna de la tabla principal, consulte configuración de faceta específica de la tabla.

Configuración de la clave externa de vinculación

El FK que vincula las tablas asignadas tiene como destino las mismas propiedades en las que se declara. Normalmente no se crea en la base de datos, ya que sería redundante. Pero hay una excepción para cuando el tipo de entidad se asigna a más de una tabla. Para cambiar sus facetas, puede usar la configuración de relaciones de Fluent API:

modelBuilder.Entity<Customer>()
    .HasOne<Customer>()
    .WithOne()
    .HasForeignKey<Customer>(a => a.Id)
    .OnDelete(DeleteBehavior.Restrict);

Limitaciones

  • La división de entidades no se puede usar para los tipos de entidad en jerarquías.
  • Para cualquier fila de la tabla principal, debe haber una fila en cada una de las tablas divididas (los fragmentos no son opcionales).

Configuración de faceta específica de tabla

Algunos patrones de asignación dan lugar a la misma propiedad CLR que se asigna a una columna en cada una de varias tablas diferentes. EF7 permite que estas columnas tengan nombres diferentes. Por ejemplo, considere una jerarquía de herencia simple:

public abstract class Animal
{
    public int Id { get; set; }
    public string Breed { get; set; } = null!;
}

public class Cat : Animal
{
    public string? EducationalLevel { get; set; }
}

public class Dog : Animal
{
    public string? FavoriteToy { get; set; }
}

Con la estrategia de asignación de herencia de TPT, estos tipos se asignarán a tres tablas. Sin embargo, la columna de clave principal de cada tabla puede tener un nombre diferente. Por ejemplo:

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

CREATE TABLE [Cats] (
    [CatId] int NOT NULL,
    [EducationalLevel] nvarchar(max) NULL,
    CONSTRAINT [PK_Cats] PRIMARY KEY ([CatId]),
    CONSTRAINT [FK_Cats_Animals_CatId] FOREIGN KEY ([CatId]) REFERENCES [Animals] ([Id]) ON DELETE CASCADE
);

CREATE TABLE [Dogs] (
    [DogId] int NOT NULL,
    [FavoriteToy] nvarchar(max) NULL,
    CONSTRAINT [PK_Dogs] PRIMARY KEY ([DogId]),
    CONSTRAINT [FK_Dogs_Animals_DogId] FOREIGN KEY ([DogId]) REFERENCES [Animals] ([Id]) ON DELETE CASCADE
);

EF7 permite configurar esta asignación mediante un generador de tablas anidadas:

modelBuilder.Entity<Animal>().ToTable("Animals");

modelBuilder.Entity<Cat>()
    .ToTable(
        "Cats",
        tableBuilder => tableBuilder.Property(cat => cat.Id).HasColumnName("CatId"));

modelBuilder.Entity<Dog>()
    .ToTable(
        "Dogs",
        tableBuilder => tableBuilder.Property(dog => dog.Id).HasColumnName("DogId"));

Con la asignación de herencia de TPC, la propiedad Breed también se puede asignar a nombres de columna diferentes en tablas diferentes. Considere, por ejemplo, las siguientes tablas TPC:

CREATE TABLE [Cats] (
    [CatId] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [CatBreed] nvarchar(max) NOT NULL,
    [EducationalLevel] nvarchar(max) NULL,
    CONSTRAINT [PK_Cats] PRIMARY KEY ([CatId])
);

CREATE TABLE [Dogs] (
    [DogId] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [DogBreed] nvarchar(max) NOT NULL,
    [FavoriteToy] nvarchar(max) NULL,
    CONSTRAINT [PK_Dogs] PRIMARY KEY ([DogId])
);

EF7 admite esta asignación de tablas:

modelBuilder.Entity<Animal>().UseTpcMappingStrategy();

modelBuilder.Entity<Cat>()
    .ToTable(
        "Cats",
        builder =>
        {
            builder.Property(cat => cat.Id).HasColumnName("CatId");
            builder.Property(cat => cat.Breed).HasColumnName("CatBreed");
        });

modelBuilder.Entity<Dog>()
    .ToTable(
        "Dogs",
        builder =>
        {
            builder.Property(dog => dog.Id).HasColumnName("DogId");
            builder.Property(dog => dog.Breed).HasColumnName("DogBreed");
        });