Mapeamento de tabela avançado

O EF Core oferece muita flexibilidade quando se trata de mapear tipos de entidade para tabelas em um banco de dados. Isso se torna ainda mais útil quando você precisa usar um banco de dados que não foi criado pelo EF.

As técnicas abaixo são descritas em termos de tabelas, mas o mesmo resultado também pode ser obtido ao mapear para exibições.

Divisão de tabela

O EF Core permite mapear duas ou mais entidades para uma única linha. Isso é chamado de divisão de tabela ou compartilhamento de tabela.

Configuração

Para usar a divisão de tabela, os tipos de entidade precisam ser mapeados para a mesma tabela, ter as chaves primárias mapeadas para as mesmas colunas e pelo menos uma relação configurada entre a chave primária de um tipo de entidade e outra na mesma tabela.

Um cenário comum para a divisão de tabela é usar apenas um subconjunto das colunas na tabela para maior desempenho ou encapsulamento.

Neste exemplo, Order representa um 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; }
}

Além da configuração necessária, chamamos Property(o => o.Status).HasColumnName("Status") para mapear DetailedOrder.Status para a mesma coluna que Order.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();
    });

Dica

Consulte o projeto de exemplo completo para obter mais contexto.

Uso

Salvar e consultar entidades usando a divisão de tabela é feito da mesma maneira que outras 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}");
}

Entidade dependente opcional

Se todas as colunas usadas por uma entidade dependente forem NULL no banco de dados, nenhuma instância será criada quando consultada. Isso permite modelar uma entidade dependente opcional, em que a propriedade de relação na entidade de segurança seria nula. Observe que isso também acontecerá se todas as propriedades do dependente forem opcionais e definidas como null, o que pode não ser esperado.

No entanto, a verificação adicional pode afetar o desempenho da consulta. Além disso, se o tipo de entidade dependente tiver seus próprios dependentes, determinar se uma instância deve ser criar se tornará não trivial. Para evitar esses problemas, o tipo de entidade dependente pode ser marcado como necessário, consulte Dependentes um para um necessários para obter mais informações.

Tokens de simultaneidade

Se qualquer um dos tipos de entidade que compartilham uma tabela tiver um token de simultaneidade, ele também deverá ser incluído em todos os outros tipos de entidade. Isso é necessário para evitar um valor de token de simultaneidade obsoleto quando apenas uma das entidades mapeadas para a mesma tabela é atualizada.

Para evitar expor o token de simultaneidade ao código de consumo, é possível criar um como uma propriedade de sombra:

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

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

Herança

É recomendável ler a página dedicada sobre herança antes de prosseguir com esta seção.

Os tipos dependentes que usam a divisão de tabela podem ter uma hierarquia de herança, mas há algumas limitações:

  • O tipo de entidade dependente não pode usar o mapeamento TPC, pois os tipos derivados não serão capazes de mapear para a mesma tabela.
  • O tipo de entidade dependente pode usar o mapeamento TPT, mas somente o tipo de entidade raiz pode usar a divisão de tabela.
  • Se o tipo de entidade de segurança usar TPC, somente os tipos de entidade que não têm descendentes poderão usar a divisão de tabela. Caso contrário, as colunas dependentes precisarão ser duplicadas nas tabelas correspondentes aos tipos derivados, complicando todas as interações.

Divisão de entidade

O EF Core permite mapear uma entidade para linhas em duas ou mais tabelas. Isso é chamado de divisão de entidade.

Configuração

Por exemplo, considere um banco de dados com três tabelas que contêm dados do cliente:

  • Uma tabela Customers para informações do cliente
  • Uma tabela PhoneNumbers para o número de telefone do cliente
  • Uma tabela Addresses para o endereço do cliente

Aqui estão as definições para essas tabelas no 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 uma dessas tabelas normalmente seria mapeada para seu próprio tipo de entidade, com relações entre os tipos. No entanto, se todas as três tabelas são sempre usadas juntas, pode ser mais conveniente mapeá-las todas para um único tipo de entidade. Por exemplo:

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; }
}

Isso é obtido no EF7 chamando SplitToTable para cada divisão no tipo de entidade. Por exemplo, o código a seguir divide o tipo de entidade Customer para as tabelas Customers, PhoneNumbers e Addresses mostradas acima:

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 também que, se necessário, diferentes nomes de coluna podem ser especificados para cada uma das tabelas. Para configurar o nome da coluna para a tabela principal, consulte Configuração de faceta específica da tabela.

Configurar a chave estrangeira de vinculação

A FK que vincula as tabelas mapeadas tem como destino as mesmas propriedades nas quais ela é declarada. Normalmente, isso não seria criado no banco de dados, pois seria redundante. Mas há uma exceção para quando o tipo de entidade é mapeado para mais de uma tabela. Para alterar suas facetas, você pode usar a API Fluente de configuração de relação:

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

Limitações

  • A divisão de entidade não pode ser usada para tipos de entidade em hierarquias.
  • Para qualquer linha na tabela principal, deve haver uma linha em cada uma das tabelas divididas (os fragmentos não são opcionais).

Configuração de faceta específica da tabela

Alguns padrões de mapeamento resultam na mesma propriedade CLR sendo mapeada para uma coluna em cada uma das várias tabelas. O EF7 permite que essas colunas tenham nomes diferentes. Por exemplo, considere uma hierarquia de herança simples:

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; }
}

Com a estratégia de mapeamento de herança TPT, esses tipos serão mapeados para três tabelas. No entanto, a coluna de chave primária em cada tabela pode ter uma nome diferente. Por exemplo:

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
);

O EF7 permite que esse mapeamento seja configurado usando um construtor de tabelas aninhado:

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"));

Com o mapeamento de herança TPC, a propriedade Breed também pode ser mapeada para nomes de colunas diferentes em tabelas diferentes. Por exemplo, considere as seguintes tabelas 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])
);

O EF7 dá suporte a este mapeamento de tabela:

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");
        });