Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
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 de Blog
regulares não têm essa propriedade.
Se você não quiser expor um DbSet
para uma ou mais entidades na hierarquia, também poderá usar a API fluente para garantir que elas estejam incluídas no modelo.
Dica
Se você não depender das 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.
Configuração de tabela por hierarquia e discriminador
Por padrão, o EF mapeia a herança usando o padrão table-per-hierarchy (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 seguinte esquema de banco de dados (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 discriminador implicitamente como uma propriedade sombra na entidade base da hierarquia. Essa propriedade pode ser configurada como qualquer outra:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property("blog_type")
.HasMaxLength(200);
}
Por fim, o discriminador também pode ser mapeado para uma propriedade .NET regular na 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 discriminadora 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. Chamada IsComplete(false)
na configuração do discriminador 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 relacional, como o SQL Server, não usarão automaticamente o predicado discriminador ao consultar colunas compartilhadas ao usar uma conversão. A consulta Url = (blog as RssBlog).Url
também retornaria o valor Url
para as linhas irmãs 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 colunas 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. Versões futuras do EF poderão permitir 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
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 .NET Data Community Standup. Como em todos os episódios do Community Standup, você pode assistir ao episódio 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 chamar modelBuilder.Entity<Blog>().UseTpcMappingStrategy()
em cada tipo de entidade raiz gerará os nomes de tabela por convenção.
Dica
Para configurar nomes de colunas 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 TPC é semelhante à estratégia TPT, exceto que uma tabela diferente é criada para cada tipo concreto na hierarquia, mas 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 esta 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 elas 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 de propriedades em tipos base é repetido para cada tipo concreto. Por exemplo, cada tabela tem uma coluna
Name
e os Cães e Gatos têm uma colunaVet
.Salvar alguns dados nesse banco de dados resulta no seguinte:
Tabela Gatos
ID | Nome | FoodId | Veterinário | Nível de Educação |
---|---|---|---|---|
1 | Alice | 99ca3e98-b26d-4a0c-d4ae-08da7aca624f | Pengelly | MBA |
2 | Mac | 99ca3e98-b26d-4a0c-d4ae-08da7aca624f | Pengelly | Pré-escolar |
oito | Baxter | 5dc5019e-6f72-454b-d4b0-08da7aca624f | Bothell Pet Hospital | Bsc |
Tabela Cães
ID | Nome | FoodId | Veterinário | FavoriteToy |
---|---|---|---|---|
3 | Notificação do sistema | 011aaf6f-d588-4fad-d4ac-08da7aca624f | Pengelly | Sr. Esquilo |
Tabela FarmAnimals
ID | Nome | FoodId | Valor | Espécies |
---|---|---|---|---|
4 | Clyde | 1d495075-f527-4498-d4af-08da7aca624f | 100,00 | Equus africanus asinus |
Tabela Humanos
ID | Nome | FoodId | FavoriteAnimalId |
---|---|---|---|
5 | Wendy | 5418fd81-7660-432f-d4b1-08da7aca624f | 2 |
6 | Artur | 59b495d4-0414-46bf-d4ad-08da7aca624f | 1 |
9 | Katie | nulo | oito |
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á nenhuma combinação de coluna e linha em qualquer 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 para a forma 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 de chave pode ser usado e nenhuma restrição adicional é necessária.
Para a estratégia TPT, sempre há 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 de outras tabelas estão vinculadas a essa tabela usando restrições de chave estrangeira.
As coisas ficam um pouco mais complicadas para o 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 do TPT, não há uma tabela comum que possa atuar como o único local em que os valores de chave residem e podem ser gerados. Isso significa que uma coluna Identity
simples não pode ser usada.
Para bancos de dados que dão 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. Essa é a estratégia usada nas tabelas TPC mostradas acima, em que 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 de 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 dão 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 dá suporte a sequências ou semente/incremento de identidade e, portanto, não há suporte para geração de valor de chave inteiro ao usar SQLite com a estratégia de TPC. No entanto, há suporte para a geração de chaves 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 de chave estrangeira 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 cão é armazenada na coluna Dogs.Id
e assim por diante. Isso significa que uma restrição de chave estrangeira não pode ser criada para esse relacionamento.
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 de chave estrangeira conterá valores de chave primária 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 diversos tipos, como consultas que envolvem o tipo base, incline-se para TPH em vez de 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 o TPT somente caso seja obrigado a fazê-lo por fatores externos.