Introdução
É quase inevitável em seu dia a dia não encontrar um cenário de polimorfismo. Me inspirei a escrever este artigo, pois hoje precisei adicionar um novo tipo de item do menu no JJInfinity. Se você não está familiarizado com polimorfismo, dê uma lida no artigo da Beatriz.
Modelagem da hierarquia de entidades
Como falar de itens do menu é mais difícil, vamos simplificar. Aproveitando que ontem lançou o último episódio de
Andor
— melhor série de Star Wars na minha opinião — iremos trabalhar com droids. Imagine um sistema que gerencia diferentes tipos de droides.
Modelamos isso com uma classe base Droid
e subclasses específicas:
public abstract class Droid
{
public Guid Id { get; set; }
public string Nome { get; set; }
public string Modelo { get; set; }
}
public class AstromechDroid : Droid
{
public bool PossuiInterfaceNave { get; set; }
}
//Esta é a marca do K-2SO, droide do Cassian Andor :)
public class KXSeriesDroid : Droid
{
public int NivelDeAutonomia { get; set; }
public bool TemBlaster { get; set; }
}
Usando Table-per-Hierarchy (TPH)
Por padrão, o EF Core utiliza TPH. A configuração adiciona uma coluna "Tipo" (discriminador) para indicar o tipo de droide no banco de dados:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity()
.HasDiscriminator("TipoDroid")
.HasValue("Astromech")
.HasValue("KXSeries");
}
Esse mapeamento usa uma única tabela Droids
, contendo todas as colunas necessárias para os diferentes
tipos, com a coluna TipoDroid
como discriminador. Na minha opinião, esse modelo vai contra um banco de
dados normalizado, pois acaba gerando muitas colunas que ficam nulas dependendo do tipo da entidade armazenada.
Se você precisa extrair cada gota de performance extra da sua aplicação, talvez acabe valendo a pena. Mas na maioria dos casos, para um servidor Web por exemplo, não vale estragar o mapeamento de suas entidades por alguns ms de performance.
Usando Table-per-Type (TPT)
Para TPT, o EF Core criará uma tabela para cada tipo concreto:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity().ToTable("Droids");
modelBuilder.Entity().ToTable("AstromechDroids");
modelBuilder.Entity().ToTable("KXSeriesDroids");
}
Nessa abordagem, a tabela base (Droids
) contém colunas comuns e as tabelas derivadas contêm apenas suas
propriedades específicas. O EF Core realiza joins para montar as entidades completas. Este é meu tipo de mapeamento
favorito e o que utilizo na maioria das vezes.
A desvantagem pouco falada, é que a query gerada pelo EF Core é bem feia, ou seja, mesmo que você precise recuperar apenas um droide, ele vai fazer um JOIN com todas as tabelas como no exemplo abaixo:
SELECT [d].[Id], [d].[Nome], [d].[Modelo], [a].[PossuiInterfaceNave], [k].[NivelDeAutonomia], [k].[TemBlaster]
FROM [Droids] AS [d]
LEFT JOIN [AstromechDroids] AS [a] ON [d].[Id] = [a].[Id]
LEFT JOIN [KXSeriesDroids] AS [k] ON [d].[Id] = [k].[Id]
WHERE [d].[Id] = @__p_0
Com apenas 2 tipos de droid, a query é bem simples. Mas imagine esse sistema rodando em Coruscant, com 150 tipos de droides diferentes — nesse cenário, talvez valha repensar a estratégia de mapeamento. No meu caso, por exemplo, no JJInfinity temos uns 5 tipos de menu, então esses JOINs extras não chegam a ser um problema.
Usando Table-per-Concrete Type (TPC)
A partir do EF Core 7.0, podemos usar TPC. Cada classe concreta terá sua própria tabela completa:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity().UseTpcMappingStrategy();
}
Todas as tabelas resultantes conterão as propriedades da classe base, o que elimina a necessidade de joins, ao custo de redundância de dados. Este é o cenário "meio termo" que existe entre TPH e TPT: as queries ficam um pouco mais simples e rápidas, mas o banco tem mais dados duplicados.
Consultando entidades polimórficas
Consultas polimórficas funcionam de forma natural no EF Core. Independente do cenário que escolheu, o código é o mesmo. Por exemplo:
var todosOsDroids = await context.Droids.ToListAsync();
O EF Core identificará os tipos corretos com base no discriminador e instanciará objetos do tipo
AstromechDroid
, KXSeriesDroid
ou outros conforme necessário.
Filtrar por tipo também é direto:
var apenasAstromechs = await context.Droids.OfType().ToListAsync();
Ou buscar droides com autonomia alta:
var droidsAutonomos = await context.Droids
.OfType()
.Where(kx => kx.NivelDeAutonomia > 7)
.ToListAsync();
Considerações de modelagem
- TPH é simples e eficiente para leituras rápidas, mas pode gerar colunas nulas e dificultar a manutenção do esquema.
- TPT mantém o esquema mais normalizado, facilita o entendimento e evita colunas nulas, porém pode impactar performance devido aos joins.
- TPC combina benefícios de ambos, eliminando joins, mas com duplicação de dados, o que pode aumentar o espaço em disco e custo de atualização.
Conclusão
Como o código C# para as queries é sempre o mesmo, a decisão final é bem voltada a modelagem de seus dados. Sua escolha deve levar em conta o equilíbrio entre performance, manutenção e modelagem do banco de dados. Para a maioria dos casos, eu particularmente recomendo o TPT pois oferece um bom meio termo, com modelagem limpa e consultas aceitáveis para poucas entidades, mas para cenários com alta demanda de performance em leitura, TPH ou TPC podem ser mais indicados.
E lembre-se: Que a Força esteja com você.