Nota
O acesso a esta página requer autorização. Podes tentar iniciar sessão ou mudar de diretório.
O acesso a esta página requer autorização. Podes tentar mudar de diretório.
O EF pode mapear uma hierarquia de tipo .NET para um banco de dados. Isso permite que você escreva suas entidades .NET em 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 problemas, etc. Os detalhes reais de como uma hierarquia de tipo é mapeada são dependentes 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 básicos ou derivados; isso significa que, se você quiser que um tipo CLR em sua hierarquia seja mapeado, deverá especificar 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 do banco de dados são automaticamente anuladas conforme necessário ao usar o mapeamento TPH. Por exemplo, a coluna RssUrl é anulável porque instâncias regulares de Blog 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 Fluent para garantir que elas sejam incluídas no modelo.
Sugestão
Se você não confia em convenções, pode 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 de tabela por hierarquia (TPH). O TPH usa uma única tabela para armazenar os dados de todos os tipos na hierarquia, e uma coluna discriminadora é 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 que tipo de Blog é armazenado em cada linha).
Você pode configurar o nome e o tipo da coluna discriminadora 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, EF adicionou o discriminador implicitamente como uma propriedade sombra na entidade base da hierarquia. Esta propriedade pode ser configurada como qualquer outra:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property("blog_type")
.HasMaxLength(200);
}
Finalmente, 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 discriminadora na consulta. Este filtro garante que não obtemos linhas adicionais para tipos base ou tipos irmãos que não estejam no resultado. Este predicado de filtro é ignorado para o tipo de entidade base, uma vez que a consulta à entidade base obterá resultados para todas as entidades na hierarquia. Ao materializar resultados de uma consulta, se nos depararmos com um valor discriminador, que não é mapeado para nenhum tipo de entidade no modelo, lançamos uma exceção, pois não sabemos como materializar os resultados. Este erro só ocorre se o banco de dados contiver linhas com valores discriminadores, que não são mapeadas no modelo EF. Se possuir tais dados, pode marcar o mapeamento do discriminador no modelo EF Core como incompleto, de forma a indicar que devemos sempre adicionar um predicado de filtro para efetuar consultas a qualquer tipo na hierarquia.
IsComplete(false) Invocar a configuração do discriminador indica que o mapeamento está incompleto.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasDiscriminator()
.IsComplete(false);
}
Colunas compartilhadas
Por padrão, quando dois tipos de entidade irmãos 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 podem 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
Os 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 transmissão. A consulta Url = (blog as RssBlog).Url também retornaria o Url valor para as linhas irmãs Blog . Para restringir a consulta a RssBlog entidades, você precisa adicionar manualmente um filtro no 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 que mapeia para esse tipo. As tabelas mapeadas para tipos derivados também armazenam uma chave estrangeira que une a tabela derivada com a tabela base.
modelBuilder.Entity<Blog>().ToTable("Blogs");
modelBuilder.Entity<RssBlog>().ToTable("RssBlogs");
Sugestão
Em vez de chamar ToTable cada tipo de entidade, você pode chamar modelBuilder.Entity<Blog>().UseTptMappingStrategy() cada tipo de entidade raiz e os nomes das tabelas serão gerados pelo EF.
Sugestão
Para configurar nomes de colunas diferentes para as colunas de chave primária em cada tabela, consulte Configuração de facetas específicas 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 somente 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();
}
Advertência
Em muitos casos, o TPT apresenta desempenho inferior quando comparado ao TPH. Consulte os documentos de desempenho para obter mais informações.
Atenção
As colunas para um tipo derivado são mapeadas para tabelas diferentes, portanto, restrições e índices FK compostos 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 TPT.
Sugestão
A equipe do EF demonstrou e falou em profundidade sobre o mapeamento TPC em um episódio do .NET Data Community Standup. Como em todos os episódios de Community Standup, você pode assistir ao episódio TPC agora no YouTube.
modelBuilder.Entity<Blog>().UseTpcMappingStrategy()
.ToTable("Blogs");
modelBuilder.Entity<RssBlog>()
.ToTable("RssBlogs");
Sugestão
Em vez de chamar ToTable em cada tipo de entidade, chamar apenas modelBuilder.Entity<Blog>().UseTpcMappingStrategy() em cada tipo de entidade raiz gerará automaticamente os nomes das tabelas por convenção.
Sugestão
Para configurar nomes de colunas diferentes para as colunas de chave primária em cada tabela, consulte Configuração de facetas específicas 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 as tabelas não são criadas para tipos abstratos - daí o nome "tabela por tipo concreto". Tal como acontece com o TPT, a própria tabela 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 nos seus tipos 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]));
Repare que:
Não há tabelas para os
Animaltipos ouPet, uma vez que estes estãoabstractno 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 seja salva no banco de dados.O mapeamento de propriedades em tipos de base é repetido para cada tipo de concreto. Por exemplo, cada tabela tem uma
Namecoluna, e tanto Cães como Gatos têm umaVetcoluna.Salvar alguns dados nesse banco de dados resulta no seguinte:
Mesa de gatos
| Id | Nome | FoodId | Veterinário | Nível de escolaridade |
|---|---|---|---|---|
| 1 | Adriana | 99ca3e98-b26d-4a0c-d4ae-08da7aca624f | Pengelly | Mestrado em Administração de Empresas (MBA) |
| 2 | Mac | 99ca3e98-b26d-4a0c-d4ae-08da7aca624f | Pengelly | Pré-escolar |
| 8 | Baxter | 5dc5019e-6f72-454b-d4b0-08da7aca624f | Bothell Pet Hospital | Licenciatura |
Mesa para cães
| Id | Nome | FoodId | Veterinário | FavoritoToy |
|---|---|---|---|---|
| 3 | Torrada | 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 de humanos
| Id | Nome | FoodId | FavoriteAnimalId |
|---|---|---|---|
| 5 | Wendy | 5418fd81-7660-432f-d4b1-08da7aca624f | 2 |
| 6 | Artur | 59b495d4-0414-46bf-d4ad-08da7aca624f | 1 |
| 9 | Kátia | 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 onde isso nunca é usado pelo modelo. Veremos abaixo como essas características podem ser importantes para consultas e armazenamento.
Geração de chaves
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, uma vez que 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 chaves pode ser usado nessa linha, e as chaves de 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 exige 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 Id que um gato. Em segundo lugar, ao contrário do 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 simples Identity não pode ser usada.
Para bancos de dados que suportam 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 usada 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 suportam sequências devem ter um padrão semelhante. Outras estratégias de geração importantes 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 e incremento apropriados, 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
O uso dessa estratégia torna mais difícil adicionar tipos derivados posteriormente, pois requer que o número total de tipos na hierarquia seja conhecido de antemão.
SQLite não suporta sequências ou semente/incremento de identidade e, portanto, a geração de valor de chave inteira não é suportada ao usar SQLite com a estratégia TPC. No entanto, a geração do lado do cliente ou chaves únicas a nível global - como GUIDs - são suportadas em qualquer base de dados, incluindo SQLite.
Restrições de chave estrangeira
A estratégia de mapeamento TPC cria um esquema SQL desnormalizado - esta é uma razão pela qual 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 é 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 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, é garantido que a coluna FK conterá valores PK válidos em todos os momentos.
Resumo e orientações
Em resumo, o TPH geralmente é bom para a maioria dos aplicativos e é um bom padrão para uma ampla gama de cenários, então não adicione a complexidade do TPC se você não precisar dele. Especificamente, se o seu código irá principalmente consultar entidades de muitos tipos, como ao escrever consultas no tipo base, prefira TPH a TPC.
Dito isso, o TPC também é uma boa estratégia de mapeamento para usar quando seu código consultará principalmente entidades de um único tipo de folha e seus benchmarks mostram uma melhoria em comparação com o TPH.
Use TPT apenas se for obrigado a fazê-lo por fatores externos.