Herança

O EF pode mapear uma hierarquia de tipos .NET para um banco de dados. Isso permite que você escreva suas entidades .NET no código como de costume, usando tipos base e derivados e faça com que o EF crie perfeitamente o esquema de banco de dados apropriado, consultas de problema etc. Os detalhes reais de como uma hierarquia de tipos é mapeada dependem do provedor; esta página descreve o suporte à herança no contexto de um banco de dados relacional.

Mapeamento de hierarquia de tipo de entidade

Por convenção, o EF não verificará automaticamente tipos base ou derivados; isso significa que, se você quiser que um tipo CLR em sua hierarquia seja mapeado, especifique explicitamente esse tipo em seu modelo. Por exemplo, especificar apenas o tipo base de uma hierarquia não fará com que o EF Core inclua implicitamente todos os seus subtipos.

O exemplo a seguir expõe um DbSet para Blog e sua subclasse RssBlog. Se Blog tiver qualquer outra subclasse, ela não será incluída no modelo.

internal class MyContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<RssBlog> RssBlogs { get; set; }
}

public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }
}

public class RssBlog : Blog
{
    public string RssUrl { get; set; }
}

Observação

As colunas de banco de dados são automaticamente tornadas anuláveis conforme necessário ao usar o mapeamento TPH. Por exemplo, a coluna RssUrl é anulável porque as instâncias regulares Blog não têm essa propriedade.

Se você não quiser expor uma DbSet para uma ou mais entidades na hierarquia, também poderá usar a API fluente para garantir que elas sejam incluídas no modelo.

Dica

Se você não depender de convenções, poderá especificar o tipo base explicitamente usando HasBaseType. Você também pode usar .HasBaseType((Type)null) para remover um tipo de entidade da hierarquia.

Tabela por hierarquia e configuração discriminatória

Por padrão, o EF mapeia a herança usando o padrão tabela por hierarquia (TPH). O TPH usa uma única tabela para armazenar os dados de todos os tipos na hierarquia e uma coluna discriminatória é usada para identificar qual tipo cada linha representa.

O modelo acima é mapeado para o esquema de banco de dados a seguir (observe a coluna Discriminator criada implicitamente, que identifica qual tipo de Blog é armazenado em cada linha).

Screenshot of the results of querying the Blog entity hierarchy using table-per-hierarchy pattern

Você pode configurar o nome e o tipo da coluna discriminatória e os valores usados para identificar cada tipo na hierarquia:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasDiscriminator<string>("blog_type")
        .HasValue<Blog>("blog_base")
        .HasValue<RssBlog>("blog_rss");
}

Nos exemplos acima, o EF adicionou o discriminatório implicitamente como uma propriedade de sombra na entidade base da hierarquia. Essa propriedade pode ser configurada como qualquer outra:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .Property("Discriminator")
        .HasMaxLength(200);
}

Por fim, o discriminador também pode ser mapeado para uma propriedade .NET regular em sua entidade:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasDiscriminator(b => b.BlogType);

    modelBuilder.Entity<Blog>()
        .Property(e => e.BlogType)
        .HasMaxLength(200)
        .HasColumnName("blog_type");
        
    modelBuilder.Entity<RssBlog>();
}

Ao consultar entidades derivadas, que usam o padrão TPH, o EF Core adiciona um predicado sobre a coluna discriminatória na consulta. Esse filtro garante que não obtenhamos linhas adicionais para tipos base ou tipos irmãos que não estão no resultado. Esse predicado de filtro é ignorado para o tipo de entidade base, pois a consulta para a entidade base obterá resultados para todas as entidades na hierarquia. Ao materializar os resultados de uma consulta, se encontrarmos um valor discriminatório, que não é mapeado para nenhum tipo de entidade no modelo, lançamos uma exceção, pois não sabemos como materializar os resultados. Esse erro só ocorrerá se o banco de dados contiver linhas com valores discriminatórios, que não são mapeadas no modelo EF. Se você tiver esses dados, poderá marcar o mapeamento discriminatório no modelo EF Core como incompleto para indicar que devemos sempre adicionar predicado de filtro para consultar qualquer tipo na hierarquia. IsComplete(false) chamada na configuração discriminatória marca o mapeamento como incompleto.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasDiscriminator()
        .IsComplete(false);
}

Colunas compartilhadas

Por padrão, quando dois tipos de entidade irmão na hierarquia tiverem uma propriedade com o mesmo nome, eles serão mapeados para duas colunas separadas. No entanto, se o tipo for idêntico, eles poderão ser mapeados para a mesma coluna de banco de dados:

public class MyContext : DbContext
{
    public DbSet<BlogBase> Blogs { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blog>()
            .Property(b => b.Url)
            .HasColumnName("Url");

        modelBuilder.Entity<RssBlog>()
            .Property(b => b.Url)
            .HasColumnName("Url");
    }
}

public abstract class BlogBase
{
    public int BlogId { get; set; }
}

public class Blog : BlogBase
{
    public string Url { get; set; }
}

public class RssBlog : BlogBase
{
    public string Url { get; set; }
}

Observação

Provedores de banco de dados relacionais, como o SQL Server, não usarão automaticamente o predicado discriminatório ao consultar colunas compartilhadas ao usar uma conversão. A consulta Url = (blog as RssBlog).Url também retornaria o valor Url das linhas irmãos Blog. Para restringir a consulta a entidades RssBlog, você precisa adicionar manualmente um filtro ao discriminador, como Url = blog is RssBlog ? (blog as RssBlog).Url : null.

Configuração de tabela por tipo

No padrão de mapeamento TPT, todos os tipos são mapeados para tabelas individuais. As propriedades que pertencem exclusivamente a um tipo base ou tipo derivado são armazenadas em uma tabela mapeada para esse tipo. As tabelas mapeadas para tipos derivados também armazenam uma chave estrangeira que une a tabela derivada à tabela base.

modelBuilder.Entity<Blog>().ToTable("Blogs");
modelBuilder.Entity<RssBlog>().ToTable("RssBlogs");

Dica

Em vez de chamar ToTable em cada tipo de entidade, você pode chamar modelBuilder.Entity<Blog>().UseTptMappingStrategy() em cada tipo de entidade raiz e os nomes de tabela serão gerados pelo EF.

Dica

Para configurar nomes de coluna diferentes para as colunas de chave primária em cada tabela, consulte configuração de faceta específica da tabela.

O EF criará o seguinte esquema de banco de dados para o modelo acima.

CREATE TABLE [Blogs] (
    [BlogId] int NOT NULL IDENTITY,
    [Url] nvarchar(max) NULL,
    CONSTRAINT [PK_Blogs] PRIMARY KEY ([BlogId])
);

CREATE TABLE [RssBlogs] (
    [BlogId] int NOT NULL,
    [RssUrl] nvarchar(max) NULL,
    CONSTRAINT [PK_RssBlogs] PRIMARY KEY ([BlogId]),
    CONSTRAINT [FK_RssBlogs_Blogs_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [Blogs] ([BlogId]) ON DELETE NO ACTION
);

Observação

Se a restrição de chave primária for renomeada, o novo nome será aplicado a todas as tabelas mapeadas para a hierarquia, as versões futuras do EF permitirão renomear a restrição apenas para uma tabela específica quando o problema 19970 for corrigido.

Se você estiver empregando a configuração em massa, poderá recuperar o nome da coluna de uma tabela específica chamando GetColumnName(IProperty, StoreObjectIdentifier).

foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
    var tableIdentifier = StoreObjectIdentifier.Create(entityType, StoreObjectType.Table);

    Console.WriteLine($"{entityType.DisplayName()}\t\t{tableIdentifier}");
    Console.WriteLine(" Property\tColumn");

    foreach (var property in entityType.GetProperties())
    {
        var columnName = property.GetColumnName(tableIdentifier.Value);
        Console.WriteLine($" {property.Name,-10}\t{columnName}");
    }

    Console.WriteLine();
}

Aviso

Em muitos casos, o TPT mostra um desempenho inferior quando comparado ao TPH. Consulte os documentos de desempenho para obter mais informações.

Cuidado

As colunas de um tipo derivado são mapeadas para tabelas diferentes, portanto, restrições FK compostas e índices que usam as propriedades herdadas e declaradas não podem ser criados no banco de dados.

Configuração de tabela por tipo concreto

Observação

O recurso TPC (tabela por tipo concreto) foi introduzido no EF Core 7.0.

No padrão de mapeamento TPC, todos os tipos são mapeados para tabelas individuais. Cada tabela contém colunas para todas as propriedades no tipo de entidade correspondente. Isso resolve alguns problemas comuns de desempenho com a estratégia de TPT.

Dica

A equipe do EF demonstrou e falou detalhadamente sobre o mapeamento de TPC em um episódio do Standup da Comunidade de Dados do .NET. Como em todos os episódios do Community Standup, você pode assistir ao episódio do TPC agora no YouTube.

modelBuilder.Entity<Blog>().UseTpcMappingStrategy()
    .ToTable("Blogs");
modelBuilder.Entity<RssBlog>()
    .ToTable("RssBlogs");

Dica

Em vez de chamar ToTable em cada tipo de entidade, apenas chamando modelBuilder.Entity<Blog>().UseTpcMappingStrategy() em cada tipo de entidade raiz gerará os nomes de tabela por convenção.

Dica

Para configurar nomes de coluna diferentes para as colunas de chave primária em cada tabela, consulte configuração de faceta específica da tabela.

O EF criará o seguinte esquema de banco de dados para o modelo acima.

CREATE TABLE [Blogs] (
    [BlogId] int NOT NULL DEFAULT (NEXT VALUE FOR [BlogSequence]),
    [Url] nvarchar(max) NULL,
    CONSTRAINT [PK_Blogs] PRIMARY KEY ([BlogId])
);

CREATE TABLE [RssBlogs] (
    [BlogId] int NOT NULL DEFAULT (NEXT VALUE FOR [BlogSequence]),
    [Url] nvarchar(max) NULL,
    [RssUrl] nvarchar(max) NULL,
    CONSTRAINT [PK_RssBlogs] PRIMARY KEY ([BlogId])
);

Esquema de banco de dados TPC

A estratégia de TPC é semelhante à estratégia de TPT, exceto que uma tabela diferente é criada para cada tipo concreto na hierarquia, mas as tabelas não são criadas para tipos abstratos, daí o nome "tabela por tipo concreto". Assim como no TPT, a tabela em si indica o tipo do objeto salvo. No entanto, ao contrário do mapeamento TPT, cada tabela contém colunas para cada propriedade no tipo concreto e seus tipos de base. Os esquemas de banco de dados TPC são desnormalizados.

Por exemplo, considere mapear essa hierarquia:

public abstract class Animal
{
    protected Animal(string name)
    {
        Name = name;
    }

    public int Id { get; set; }
    public string Name { get; set; }
    public abstract string Species { get; }

    public Food? Food { get; set; }
}

public abstract class Pet : Animal
{
    protected Pet(string name)
        : base(name)
    {
    }

    public string? Vet { get; set; }

    public ICollection<Human> Humans { get; } = new List<Human>();
}

public class FarmAnimal : Animal
{
    public FarmAnimal(string name, string species)
        : base(name)
    {
        Species = species;
    }

    public override string Species { get; }

    [Precision(18, 2)]
    public decimal Value { get; set; }

    public override string ToString()
        => $"Farm animal '{Name}' ({Species}/{Id}) worth {Value:C} eats {Food?.ToString() ?? "<Unknown>"}";
}

public class Cat : Pet
{
    public Cat(string name, string educationLevel)
        : base(name)
    {
        EducationLevel = educationLevel;
    }

    public string EducationLevel { get; set; }
    public override string Species => "Felis catus";

    public override string ToString()
        => $"Cat '{Name}' ({Species}/{Id}) with education '{EducationLevel}' eats {Food?.ToString() ?? "<Unknown>"}";
}

public class Dog : Pet
{
    public Dog(string name, string favoriteToy)
        : base(name)
    {
        FavoriteToy = favoriteToy;
    }

    public string FavoriteToy { get; set; }
    public override string Species => "Canis familiaris";

    public override string ToString()
        => $"Dog '{Name}' ({Species}/{Id}) with favorite toy '{FavoriteToy}' eats {Food?.ToString() ?? "<Unknown>"}";
}

public class Human : Animal
{
    public Human(string name)
        : base(name)
    {
    }

    public override string Species => "Homo sapiens";

    public Animal? FavoriteAnimal { get; set; }
    public ICollection<Pet> Pets { get; } = new List<Pet>();

    public override string ToString()
        => $"Human '{Name}' ({Species}/{Id}) with favorite animal '{FavoriteAnimal?.Name ?? "<Unknown>"}'" +
           $" eats {Food?.ToString() ?? "<Unknown>"}";
}

Ao usar o SQL Server, as tabelas criadas para essa hierarquia são:

CREATE TABLE [Cats] (
    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [Name] nvarchar(max) NOT NULL,
    [FoodId] uniqueidentifier NULL,
    [Vet] nvarchar(max) NULL,
    [EducationLevel] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Cats] PRIMARY KEY ([Id]));

CREATE TABLE [Dogs] (
    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [Name] nvarchar(max) NOT NULL,
    [FoodId] uniqueidentifier NULL,
    [Vet] nvarchar(max) NULL,
    [FavoriteToy] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Dogs] PRIMARY KEY ([Id]));

CREATE TABLE [FarmAnimals] (
    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [Name] nvarchar(max) NOT NULL,
    [FoodId] uniqueidentifier NULL,
    [Value] decimal(18,2) NOT NULL,
    [Species] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_FarmAnimals] PRIMARY KEY ([Id]));

CREATE TABLE [Humans] (
    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [Name] nvarchar(max) NOT NULL,
    [FoodId] uniqueidentifier NULL,
    [FavoriteAnimalId] int NULL,
    CONSTRAINT [PK_Humans] PRIMARY KEY ([Id]));

Observe que:

  • Não há tabelas para os tipos Animal ou Pet, pois eles são abstract no modelo de objeto. Lembre-se de que o C# não permite instâncias de tipos abstratos e, portanto, não há nenhuma situação em que uma instância de tipo abstrato será salva no banco de dados.

  • O mapeamento das propriedades em tipos base é repetido para cada tipo de concreto. Por exemplo, cada tabela tem uma coluna Name, e Gatos e Cães têm uma coluna Vet.

  • Salvar alguns dados nesse banco de dados resulta no seguinte:

Tabela de gatos

ID Nome FoodId Vet EducationLevel
1 Alice 99ca3e98-b26d-4a0c-d4ae-08da7aca624f Cupcake MBA
2 Mac 99ca3e98-b26d-4a0c-d4ae-08da7aca624f Cupcake Pré-escolar
8 Baxter 5dc5019e-6f72-454b-d4b0-08da7aca624f Clínica Veterinária Miau Bacharel em Ciência

Tabela de cães

ID Nome FoodId Vet FavoriteToy
3 Notificação do sistema 011aaf6f-d588-4fad-d4ac-08da7aca624f Cupcake Sr. Esquilo

Tabela FarmAnimals

ID Nome FoodId Valor Espécie
4 Chico 1d495075-f527-4498-d4af-08da7aca624f 100.00 Equus africanus asinus

Tabela de humanos

ID Nome FoodId FavoriteAnimalId
5 Wendy 5418fd81-7660-432f-d4b1-08da7aca624f 2
6 Arthur 59b495d4-0414-46bf-d4ad-08da7aca624f 1
9 Katie nulo 8

Observe que, ao contrário do mapeamento TPT, todas as informações de um único objeto estão contidas em uma única tabela. E, ao contrário do mapeamento TPH, não há combinação de coluna e linha em nenhuma tabela em que isso nunca seja usado pelo modelo. Veremos abaixo como essas características podem ser importantes para consultas e armazenamento.

Geração de chave

A estratégia de mapeamento de herança escolhida tem consequências sobre como os valores de chave primária são gerados e gerenciados. As chaves no TPH são fáceis, pois cada instância de entidade é representada por uma única linha em uma única tabela. Qualquer tipo de geração de valor chave pode ser usado, e nenhuma restrição adicional é necessária.

Para a estratégia TPT, há sempre uma linha na tabela mapeada para o tipo base da hierarquia. Qualquer tipo de geração de chave pode ser usado nessa linha, e as chaves para outras tabelas são vinculadas a essa tabela usando restrições de chave estrangeira.

As coisas ficam um pouco mais complicadas para a TPC. Primeiro, é importante entender que o EF Core requer que todas as entidades em uma hierarquia tenham um valor de chave exclusivo, mesmo que as entidades tenham tipos diferentes. Por exemplo, usando nosso modelo de exemplo, um Cão não pode ter o mesmo valor de chave de ID que um Gato. Em segundo lugar, ao contrário da TPT, não há uma tabela comum que possa atuar como o único lugar onde os valores-chave vivem e podem ser gerados. Isso significa que uma coluna Identity simples não pode ser usada.

Para bancos de dados que oferecem suporte a sequências, os valores de chave podem ser gerados usando uma única sequência referenciada na restrição padrão para cada tabela. Esta é a estratégia utilizada nas tabelas TPC mostradas acima, onde cada tabela tem o seguinte:

[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence])

AnimalSequence é uma sequência de banco de dados criada pelo EF Core. Essa estratégia é usada por padrão para hierarquias TPC ao usar o provedor de banco de dados EF Core para SQL Server. Os provedores de banco de dados para outros bancos de dados que oferecem suporte a sequências devem ter um padrão semelhante. Outras estratégias de geração chave que usam sequências, como padrões Hi-Lo, também podem ser usadas com TPC.

Embora as colunas de identidade padrão não funcionem com TPC, é possível usar colunas de identidade se cada tabela estiver configurada com uma semente apropriada e incrementar de modo que os valores gerados para cada tabela nunca entrem em conflito. Por exemplo:

modelBuilder.Entity<Cat>().ToTable("Cats", tb => tb.Property(e => e.Id).UseIdentityColumn(1, 4));
modelBuilder.Entity<Dog>().ToTable("Dogs", tb => tb.Property(e => e.Id).UseIdentityColumn(2, 4));
modelBuilder.Entity<FarmAnimal>().ToTable("FarmAnimals", tb => tb.Property(e => e.Id).UseIdentityColumn(3, 4));
modelBuilder.Entity<Human>().ToTable("Humans", tb => tb.Property(e => e.Id).UseIdentityColumn(4, 4));

Importante

Usar essa estratégia torna mais difícil adicionar tipos derivados mais tarde, pois exige que o número total de tipos na hierarquia seja conhecido com antecedência.

O SQLite não oferece suporte a sequências ou semente/incremento de identidade e, portanto, a geração de valor de chave inteira não é suportada ao usar o SQLite com a estratégia TPC. No entanto, há suporte para a geração do lado do cliente ou chaves globalmente exclusivas, como GUIDs, em qualquer banco de dados, incluindo SQLite.

Restrições de chave estrangeira

A estratégia de mapeamento de TPC cria um esquema SQL desnormalizado – esse é um dos motivos pelos quais alguns puristas de banco de dados são contra. Por exemplo, considere a coluna de chave estrangeira FavoriteAnimalId. O valor nesta coluna deve corresponder ao valor da chave primária de algum animal. Isso pode ser imposto no banco de dados com uma restrição FK simples ao usar TPH ou TPT. Por exemplo:

CONSTRAINT [FK_Animals_Animals_FavoriteAnimalId] FOREIGN KEY ([FavoriteAnimalId]) REFERENCES [Animals] ([Id])

Mas, ao usar TPC, a chave primária para qualquer animal determinado é armazenada na tabela correspondente ao tipo concreto desse animal. Por exemplo, a chave primária de um gato é armazenada na coluna Cats.Id, enquanto a chave primária de um cachorro é armazenada na coluna Dogs.Id e assim por diante. Isso significa que uma restrição FK não pode ser criada para essa relação.

Na prática, isso não é um problema, desde que o aplicativo não tente inserir dados inválidos. Por exemplo, se todos os dados forem inseridos pelo EF Core e usarem navegações para relacionar entidades, será garantido que a coluna FK conterá valores PK válidos o tempo todo.

Resumo e diretrizes

Em resumo, o TPH geralmente é bom para a maioria dos aplicativos e é um bom padrão para uma ampla variedade de cenários, portanto, não adicione a complexidade do TPC se você não precisar dele. Especificamente, se o seu código consultar principalmente entidades de muitos tipos, como escrever consultas no tipo base, escolha TPH ao invés da TPC.

Dito isto, o TPC também é uma boa estratégia de mapeamento a ser usada quando seu código consultará principalmente entidades de um único tipo de folha e seus parâmetros de comparação mostram uma melhoria em comparação com o TPH.

Use a TPT somente se for restringido a fazê-lo por fatores externos.