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).
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
ouPet
, pois eles sãoabstract
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 colunaVet
.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.