Start Debugging

TPH vs TPT vs TPC para mapeamento de herança no EF Core 11: qual você deve escolher?

No EF Core 11, use TPH por padrao para quase toda hierarquia, recorra ao TPC apenas quando voce consulta quase sempre um unico tipo folha e um benchmark prova que ele vence, e use TPT somente quando uma restricao externa obrigar.

No EF Core 11 (com .NET 11 e C# 14), mapeie uma hierarquia de classes com table-per-hierarchy (TPH) a menos que voce tenha um motivo medido para nao fazer isso. O TPH coloca toda a hierarquia em uma unica tabela com uma coluna discriminadora, entao as leituras sao varreduras de tabela unica sem joins. Recorra ao table-per-concrete-type (TPC) apenas quando seu codigo consulta de forma esmagadora um unico tipo folha e um benchmark sobre seus dados mostra que ele supera o TPH. Use table-per-type (TPT) somente quando uma restricao externa obrigar, porque o proprio benchmark da Microsoft coloca o TPT em cerca de duas vezes o tempo e quase o dobro das alocacoes do TPH em uma consulta do tipo base. A regra em uma linha: TPH por padrao, TPC para cargas de trabalho centradas em tipo folha que medem mais rapido, TPT nunca por escolha.

Este artigo e a decisao, nao o passo a passo completo de configuracao. Se voce quer a API do discriminador, as colunas compartilhadas e a mecanica de colunas anulaveis em profundidade, leia como configurar o mapeamento de heranca table-per-hierarchy (TPH) no EF Core 11. Aqui colocamos as tres estrategias lado a lado, mostramos o esquema que cada uma gera e nomeamos as restricoes que tomam a decisao por voce.

A matriz de recursos em uma tela

Pegue uma hierarquia de dois niveis: uma classe base Blog e uma derivada RssBlog que adiciona um RssUrl. As tres estrategias mapeiam isso para tres esquemas completamente diferentes, e cada compromisso abaixo deriva dessa forma.

DimensaoTPHTPTTPC
Tabelas geradasuma, toda a hierarquiauma por tipo (incluindo abstratos)uma por tipo concreto apenas
Coluna discriminadorasimnaonao
Colunas de tipo derivadoanulaveis, tabela compartilhadatabela propria, podem ser NOT NULLtabela propria, podem ser NOT NULL
Consulta do tipo base (context.Blogs)um SELECT, sem joinLEFT JOIN entre todas as tabelasUNION ALL entre tabelas concretas
Consulta de um tipo folha (OfType<RssBlog>)predicado discriminadorjoin tabela base + folhauma tabela, sem filtro
Forma de armazenamentolarga, esparsa, muitos nullsnormalizada, sem nullsdesnormalizada, colunas repetidas
Geracao de chavesqualquer (Identity serve)qualquer (Identity na base)sequencia compartilhada, sem Identity simples
Restricao FK para o tipo basesimsimnao (a chave vive na tabela folha)
Tipos complexos / colunas JSONsimsim (novo no EF Core 11)sim (novo no EF Core 11)
Leitura do tipo base: velocidade relativamais rapida (referencia)~2x mais lenta~igual ao TPH
Postura da Microsoftpadrao recomendado”apenas se voce estiver obrigado”boa para consultas de um tipo folha

O padrao nao e sutil. O TPH vence ou empata em quase toda linha que importa, o TPC o iguala exceto quando voce consulta entre tipos, e o TPT troca um esquema de aparencia mais limpa por joins que custam a voce em tempo de consulta. Tres dessas celulas mudaram no EF Core 11: os tipos complexos e as colunas JSON agora funcionam em hierarquias TPT e TPC, o que antes nao era suportado e empurrava as pessoas de volta as entidades proprietarias para qualquer objeto de valor herdado. Isso encerra um dos ultimos motivos nao relacionados a desempenho para evitar TPT e TPC, mas nao muda o veredito de desempenho.

O que cada estrategia realmente escreve no banco de dados

Os esquemas concretizam os compromissos abstratos. O TPH e uma unica tabela com um discriminador e colunas derivadas anulaveis:

-- TPH: EF Core 11, SQL Server
CREATE TABLE [Blogs] (
    [BlogId] int NOT NULL IDENTITY,
    [Url] nvarchar(max) NULL,
    [Discriminator] nvarchar(max) NOT NULL,
    [RssUrl] nvarchar(max) NULL,          -- nullable: base Blogs have no RssUrl
    CONSTRAINT [PK_Blogs] PRIMARY KEY ([BlogId])
);

O TPT divide cada tipo em sua propria tabela, ligadas por uma chave estrangeira sobre a chave primaria compartilhada:

-- TPT: EF Core 11, SQL Server
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
);

O TPC da a cada tipo concreto uma tabela autocontida com cada coluna herdada repetida, com chave baseada em uma sequencia compartilhada:

-- TPC: EF Core 11, SQL Server
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,             -- inherited column, repeated here
    [RssUrl] nvarchar(max) NULL,
    CONSTRAINT [PK_RssBlogs] PRIMARY KEY ([BlogId])
);

Configurar cada uma e uma unica linha na entidade raiz. O TPH e o padrao e nao precisa de nada; TPT e TPC sao ativados com uma chamada de estrategia de mapeamento:

// EF Core 11: choosing a strategy on the root entity type
modelBuilder.Entity<Blog>().UseTphMappingStrategy(); // default, can be omitted
modelBuilder.Entity<Blog>().UseTptMappingStrategy(); // one table per type
modelBuilder.Entity<Blog>().UseTpcMappingStrategy(); // one table per concrete type

Quando escolher TPH

O TPH e a resposta certa para a grande maioria das hierarquias. Escolha-o quando:

O unico custo que voce aceita e que uma propriedade obrigatoria em um tipo derivado ainda mapeia para uma coluna anulavel, porque as linhas irmas a deixam vazia. Se a nao-nulidade imposta pelo banco de dados em propriedades derivadas e um requisito rigido, esse e o motivo classico para deixar o TPH, e ele aponta para o TPT.

Quando escolher TPC

O TPC e o especialista. Ele iguala o TPH de perto em consultas entre tipos e se adianta em uma forma especifica:

O preco e um esquema desnormalizado e chaves incomodas. O TPC nao pode usar uma coluna Identity simples, porque nao ha uma unica tabela que possua a sequencia; o EF Core 11 usa por padrao uma sequencia de banco de dados compartilhada (NEXT VALUE FOR [BlogSequence]) para que as chaves continuem unicas entre tabelas irmas. No SQLite, que nao tem sequencias, a geracao de chaves inteiras nao esta disponivel para o TPC e voce recorre a GUIDs gerados no cliente. E como a chave primaria de um tipo base pode viver em qualquer tabela concreta, uma chave estrangeira que referencia o tipo base nao pode ser imposta por uma restricao de banco de dados de forma alguma. Se todas as suas escritas passam pelo EF Core com navegacoes, isso costuma ser aceitavel, mas e uma perda real de integridade em nivel de banco de dados.

Quando escolher TPT (e por que a resposta costuma ser “nao escolha”)

O TPT produz o esquema que mais se parece com seu diagrama de classes: uma tabela por tipo, unidas pela chave. Essa estetica e a armadilha. Recorra ao TPT apenas quando:

Nao escolha o TPT porque ele parece mais limpo. Cada consulta do tipo base faz join entre todo o conjunto de tabelas, e joins sao uma das principais fontes de problemas de desempenho relacional. Os numeros confirmam isso, que e a proxima secao.

O benchmark: o TPT custa cerca de 2x

Isso nao e conversa fiada. O proprio benchmark de heranca da Microsoft monta uma hierarquia de 7 tipos, semeia 5000 linhas por tipo (35000 linhas no total) e carrega cada linha do banco de dados. Os resultados:

MetodoMediaAlocado
TPH149.0 ms40 MB
TPT312.9 ms75 MB
TPC158.2 ms46 MB

O TPT e cerca de 2.1x mais lento que o TPH e aloca quase o dobro de memoria, porque carregar a hierarquia faz join de sete tabelas. O TPC fica dentro de cerca de 6 por cento do TPH nesta consulta de todos os tipos, e se adiantaria ao TPH em uma consulta de um unico tipo folha onde ele le uma tabela e o TPH ainda varre a tabela compartilhada com um filtro discriminador. A metodologia importa: esta e uma consulta do tipo base que toca cada tabela, que e o pior caso do TPC e do TPT, entao a diferenca que voce ve na sua carga de trabalho depende de com que frequencia voce consulta entre tipos versus um tipo folha. Ainda assim, a conclusao e estavel entre execucoes: o TPT paga um imposto de join que o TPH e o TPC nao pagam, e nenhum argumento de estetica de esquema o recupera.

Execute o benchmark contra seu proprio modelo antes de tomar uma decisao irreversivel. Mudar uma estrategia de heranca depois de ter dados em producao significa uma migracao de esquema que move linhas entre tabelas, entao esta e uma decisao que vale a pena medir uma vez, cedo.

As armadilhas que decidem por voce

Tres restricoes podem decidir a estrategia independentemente da preferencia.

A primeira e a nao-nulidade imposta pelo banco de dados em uma propriedade derivada. O TPH nao consegue fazer isso, porque a coluna compartilhada precisa ser anulavel para as linhas irmas. Se voce precisa que o banco de dados (nao apenas sua aplicacao) garanta que cada CardPayment tem um Last4, voce precisa dessa coluna em sua propria tabela, o que significa TPT ou TPC.

A segunda e a geracao de chaves no seu banco de dados. O TPC precisa de sequencias para chaves inteiras. No SQL Server isso e automatico, mas no SQLite voce nao pode usar chaves de identidade inteiras com o TPC de forma alguma e deve trocar para GUIDs. Se voce esta no SQLite e quer chaves inteiras, o TPC esta descartado.

A terceira e a integridade de chave estrangeira para o tipo base. Se outras tabelas referenciam seu tipo base e voce quer que o banco de dados imponha essas referencias, o TPC nao consegue lhe dar a restricao. TPH e TPT conseguem. Isso por si so descarta o TPC para muitos esquemas normalizados.

Uma coisa igual nas tres: voce nao pode mudar o tipo de uma entidade em tempo de execucao. Transformar um CardPayment em um BankTransferPayment e um delete mais um insert em cada estrategia, porque o discriminador (ou a propria tabela) codifica o tipo. Isso e uma realidade de modelagem, nao um diferenciador.

A recomendacao, dita claramente

Use TPH por padrao. E a mais rapida para a consulta comum entre tipos, a mais simples para escrever contra ela, a unica estrategia sem friccao de geracao de chaves, e o padrao recomendado pela Microsoft para um amplo leque de cenarios. Recorra ao TPC apenas quando sua carga de trabalho for dominada por consultas de um unico tipo folha e um benchmark sobre seus dados mostrar que ele supera o TPH, e aceite o esquema desnormalizado, as chaves de sequencia compartilhada e a ausencia da restricao FK para o tipo base que o acompanham. Use TPT apenas quando um fator externo nao lhe der escolha, e faca isso sabendo que voce paga um imposto de consulta de cerca de 2x por um esquema que parece mais arrumado.

O modelo mental e o mesmo que os numeros impoem: uma tabela e rapida, muitas tabelas unidas por join sao lentas, e muitas tabelas sem join sao rapidas mas desnormalizadas. Se esta decisao faz parte de uma atualizacao de versao mais ampla, as mudancas de heranca e mapeamento tendem a aparecer junto as do guia de migracao do EF Core 6 para o EF Core 11.

Leituras relacionadas

Fontes

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Voltar