EF Core'da inheritance (kalıtım) eşlemesi, C# tarafındaki temel sınıf ve türetilmiş sınıf hiyerarşisini veritabanına yansıtmanın yoludur. Entity Framework Core üç strateji destekler: bütün hiyerarşiyi tek tabloda tutan TPH (table-per-hierarchy, varsayılan), her türü ayrı tabloya koyan TPT (table-per-type) ve her somut türü bütün kolonlarıyla ayrı tabloya koyan TPC (table-per-concrete-type). Belgelerin genel önerisi TPH ile başlamak, TPC'yi belirli sorgu kalıpları için düşünmek ve TPT'yi yalnızca dış bir zorunluluk varsa kullanmaktır.
Bu yazıda hiyerarşinin modele nasıl dahil edildiğini, discriminator kolonunun yapılandırmasını, TPT ve TPC şemalarını, TPC'de anahtar üretimi ve foreign key kısıtı sorunlarını, belgelerdeki performans karşılaştırmasını ve sık yapılan hataları anlatıyoruz. Belgeler kalıtım eşlemesinin ayrıntılarının veritabanı sağlayıcısına bağlı olduğunu belirtir; burada anlatılanlar ilişkisel veritabanları içindir. Serinin tamamı için EF Core öğrenme rehberi sayfasına bakabilirsiniz.
Hiyerarşiyi modele dahil etmek

İlk bilinmesi gereken kural şudur: EF temel ya da türetilmiş türleri kendiliğinden taramaz. Hiyerarşideki bir türün eşlenmesini istiyorsanız onu modelde açıkça belirtmelisiniz. Yalnızca temel türü modele koymak, onun alt türlerinin de dahil edileceği anlamına gelmez. Belgelerdeki örnekte hem Blog hem de alt sınıfı RssBlog için birer DbSet açılır; Blog'un başka bir alt sınıfı varsa o modele girmez:
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; }
}Bir tür için DbSet açmak istemiyorsanız onu modelBuilder.Entity<RssBlog>() ile Fluent API üzerinden modele ekleyebilirsiniz. Konvansiyonlara güvenmiyorsanız temel türü HasBaseType ile açıkça verebilir, HasBaseType((Type)null) ile bir türü hiyerarşiden çıkarabilirsiniz. Model yapılandırmasının genel kuralları için model yapılandırma yazısına bakın.
TPH: tek tablo ve discriminator
EF kalıtımı varsayılan olarak TPH ile eşler. Hiyerarşideki bütün türlerin verisi tek bir tabloda durur ve her satırın hangi türe ait olduğunu bir discriminator kolonu gösterir. Yukarıdaki model için EF örtük olarak Discriminator adında bir kolon oluşturur. Türetilmiş türe özgü kolonlar (örnekteki RssUrl) gerektiğinde kendiliğinden nullable yapılır, çünkü düz Blog satırlarında bu değer yoktur.
Discriminator kolonunun adını, tipini ve her türü temsil eden değerleri yapılandırabilirsiniz:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasDiscriminator<string>("blog_type")
.HasValue<Blog>("blog_base")
.HasValue<RssBlog>("blog_rss");
}Bu örnekte discriminator temel türe bir shadow property olarak eklenir ve diğer özellikler gibi yapılandırılabilir; örneğin Property("blog_type").HasMaxLength(200) ile uzunluğu sınırlanabilir. İsterseniz discriminator'ı sınıftaki normal bir özelliğe de eşleyebilirsiniz: HasDiscriminator(b => b.BlogType).
Türetilmiş bir türü sorguladığınızda EF sorguya discriminator kolonu üzerinde bir koşul ekler; böylece temel türün ya da kardeş türlerin satırları sonuca karışmaz. Temel türü sorgularken bu koşul atlanır, çünkü zaten bütün hiyerarşi istenmektedir. Sorgu sonucunda modelde hiçbir türe eşlenmemiş bir discriminator değeriyle karşılaşılırsa EF satırı hangi nesneye dönüştüreceğini bilemez ve hata fırlatır. Veritabanınızda böyle satırlar varsa (örneğin tabloyu başka bir uygulama da yazıyorsa) eşlemeyi eksik olarak işaretleyin; EF o zaman her tür için discriminator filtresini ekler:
modelBuilder.Entity<Blog>()
.HasDiscriminator()
.IsComplete(false);Kardeş türlerde ortak kolon
Varsayılan olarak hiyerarşide aynı adlı özelliğe sahip iki kardeş tür iki ayrı kolona eşlenir. Tipleri aynıysa HasColumnName("Url") ile ikisini aynı kolona eşleyebilirsiniz. Belgeler burada bir tuzağa dikkat çeker: SQL Server gibi ilişkisel sağlayıcılar, cast içeren bir sorguda ortak kolon için discriminator koşulunu kendiliğinden eklemez. (blog as RssBlog).Url ifadesi kardeş Blog satırlarının Url değerini de döndürebilir; sonucu yalnızca RssBlog ile sınırlamak için blog is RssBlog ? (blog as RssBlog).Url : null biçiminde açık bir tür kontrolü yazmanız gerekir.
TPT: her tür için ayrı tablo
TPT'de hiyerarşideki her tür ayrı bir tabloya eşlenir. Bir özellik hangi türde tanımlıysa o türün tablosunda saklanır; türetilmiş türlerin tabloları, temel tabloya bağlanan bir foreign key taşır. Yapılandırma için her türe ayrı tablo adı verilir ya da kök türde UseTptMappingStrategy() çağrılarak tablo adlarının EF tarafından üretilmesi sağlanır:
modelBuilder.Entity<Blog>().ToTable("Blogs");
modelBuilder.Entity<RssBlog>().ToTable("RssBlogs");Belgelerin bu model için gösterdiği SQL Server şemasında RssBlogs tablosunun birincil anahtarı aynı zamanda Blogs tablosunu gösteren bir foreign key'dir:
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
);TPT şeması nesne modeline en çok benzeyen şemadır ve bu yüzden sezgisel olarak "temiz" görünür. Ancak belgelerin iki uyarısı var. Birincisi performanstır: TPT çoğu durumda TPH'den daha yavaştır, çünkü bir nesneyi okumak birden çok tabloyu birleştirmeyi gerektirir. İkincisi kısıtlardır: türetilmiş türün kolonları farklı tablolarda durduğu için hem miras alınan hem de türde tanımlanan özellikleri birlikte kullanan bileşik foreign key kısıtları ve indeksler veritabanında oluşturulamaz. Ayrıca birincil anahtar kısıtının adını değiştirirseniz yeni ad hiyerarşideki bütün tablolara uygulanır; belgelere göre yalnızca belirli bir tablo için değiştirme olanağı ileride gelecek.
TPC: her somut tür için tam tablo

TPC'de de her tür ayrı tabloya gider, ama iki fark vardır: tablolar yalnızca somut (abstract olmayan) türler için oluşturulur ve her tablo, türün kendi özelliklerine ek olarak temel türlerden gelen bütün özelliklerin kolonlarını da içerir. Yani şema bilinçli olarak denormalize edilmiştir. TPT gibi tablo, kaydın türünü zaten gösterir; ama TPT'den farklı olarak bir nesnenin bütün verisi tek bir tabloda bulunur. TPH'den farklı olarak da hiçbir tabloda modelin hiç kullanmadığı bir kolon ve satır birleşimi kalmaz.
modelBuilder.Entity<Blog>().UseTpcMappingStrategy()
.ToTable("Blogs");
modelBuilder.Entity<RssBlog>()
.ToTable("RssBlogs");Belgelerdeki büyük örnekte Animal ve Pet abstract, Cat, Dog, FarmAnimal ve Human somut türlerdir. Sonuçta yalnızca Cats, Dogs, FarmAnimals ve Humans tabloları oluşur; Animal veya Pet için tablo yoktur, çünkü C# abstract türlerin örneğine izin vermez ve böyle bir örnek hiçbir zaman kaydedilmez. Temel türden gelen Name kolonu her tabloda tekrarlanır; Vet kolonu hem Cats hem Dogs tablosunda bulunur.
TPC ve anahtar üretimi
Kalıtım stratejisi birincil anahtarların nasıl üretileceğini doğrudan etkiler. TPH'de her nesne tek tablodaki tek bir satırdır; her türlü anahtar üretimi kullanılabilir. TPT'de temel tabloda her zaman bir satır vardır; anahtar orada üretilir, diğer tablolar ona foreign key ile bağlanır. TPC'de iş zorlaşır, çünkü EF Core bir hiyerarşideki bütün entity'lerin, türleri farklı olsa bile, benzersiz anahtar değerine sahip olmasını şart koşar: bir Dog ile bir Cat aynı Id'ye sahip olamaz. Ortak bir tablo olmadığı için basit bir identity kolonu da kullanılamaz.
Sequence destekleyen veritabanlarında çözüm, bütün tabloların varsayılan değerinde tek bir sequence kullanmaktır. Belgelere göre SQL Server sağlayıcısı TPC hiyerarşileri için bunu varsayılan olarak yapar ve EF Core bir AnimalSequence oluşturur; sequence destekleyen diğer veritabanlarının sağlayıcılarında da benzer bir varsayılan beklenir. Hi-Lo gibi sequence tabanlı diğer stratejiler de kullanılabilir. Sequence'ların genel yapılandırması değer üretimi yazısında anlatılıyor.
[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence])Standart identity kolonları TPC ile çalışmaz, ama her tabloya çakışmayacak bir başlangıç ve artış değeri verirseniz identity kullanabilirsiniz. Belgeler bunun, hiyerarşideki tür sayısının önceden bilinmesini gerektirdiği için sonradan yeni tür eklemeyi zorlaştırdığını özellikle belirtir:
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));TPC ve foreign key kısıtları
Denormalize şemanın bir bedeli daha vardır. Belgelerdeki Human.FavoriteAnimal ilişkisini düşünün: FavoriteAnimalId kolonundaki değer herhangi bir hayvanın birincil anahtarı olmalıdır. TPH ya da TPT'de bu, ortak tabloya giden basit bir foreign key kısıtıyla sağlanır. TPC'de ise bir kedinin anahtarı Cats.Id, bir köpeğinki Dogs.Id kolonunda durur; tek bir tabloyu gösteren bir kısıt yazılamaz, dolayısıyla bu ilişki için foreign key kısıtı oluşturulamaz. Belgelere göre bu, uygulama geçersiz veri eklemeye çalışmadığı sürece pratikte sorun olmaz: bütün veriyi EF Core ekliyor ve ilişkileri navigation'larla kuruyorsanız kolon her zaman geçerli değerler içerir. Veritabanına EF dışından yazan başka sistemler varsa bu güvenceyi kaybettiğinizi hesaba katın.
Üç stratejinin karşılaştırması
| Özellik | TPH | TPT | TPC |
|---|---|---|---|
| Tablo sayısı | Hiyerarşi başına bir | Her tür için bir | Her somut tür için bir |
| Tür bilgisi | Discriminator kolonu | Tablonun kendisi | Tablonun kendisi |
| Türe özgü kolonlar | Nullable olur | Kendi tablosunda | Her tabloda tam kopya |
| Okuma | Tek tablo | Join gerekir | Tek tür için tek tablo |
| Anahtar üretimi | Serbest | Temel tabloda | Sequence veya ayarlı identity |
| Ortak FK kısıtı | Var | Var | Kurulamaz |
| Belgelerin önerisi | Varsayılan tercih | Yalnızca zorunluysa | Tek yaprak türü sorgularken |
Performans: belgeler ne diyor?
Performans belgesi strateji seçiminin uygulama performansını ciddi biçimde etkileyebileceğini söyler ve karar vermeden önce ölçmeyi önerir. TPH'deki boş kolonlar ilk bakışta yer kaplıyor ve yavaşlatıyor gibi görünse de belgelere göre ölçümler TPT'nin çoğu durumda daha kötü olduğunu gösterir: TPH'de bütün veri tek tablodan gelirken TPT sorguları birden çok tabloyu birleştirmek zorundadır ve join'ler ilişkisel veritabanlarında performans sorunlarının başlıca kaynaklarındandır. Veritabanları boş kolonlarla genellikle iyi başa çıkar; SQL Server'daki sparse column özelliği seyrek dolu TPH kolonlarının maliyetini daha da azaltabilir.
TPC'nin performansı TPH'ye benzer; bütün türleri seçerken birden çok tablo devreye girdiği için biraz daha yavaştır, ama tek bir yaprak türü sorgularken öne çıkar, çünkü sorgu tek tabloyu kullanır ve filtreye ihtiyaç duymaz. Belgelerdeki örnek benchmark yedi türlü bir hiyerarşide tür başına 5000, toplam 35000 satırın tamamını yükler; bu senaryoda ortalama süreler TPH için 149,0 ms, TPT için 312,9 ms, TPC için 158,2 ms olarak verilir. Belgeler sonuçların her zaman çalıştırılan sorguya ve hiyerarşideki tablo sayısına bağlı olduğunu, başka sorgularda farkın değişebileceğini ve bu benchmark kodunu kendi sorgularınız için şablon olarak kullanmanızı önerir. Genel performans yaklaşımı için EF Core performans yazısına bakın.
Hangi stratejiyi seçmeli?
Belgelerin özet kılavuzu üç cümleye sığar. TPH çoğu uygulama için yeterlidir ve geniş bir senaryo yelpazesi için iyi bir varsayılandır; gerekmiyorsa TPC'nin karmaşıklığını eklemeyin. Kodunuz çoğunlukla temel tür üzerinden, yani birçok türün entity'lerini birlikte sorguluyorsa TPH'ye yönelin. Kodunuz çoğunlukla tek bir yaprak türün entity'lerini sorguluyorsa ve kendi ölçümleriniz TPH'ye göre iyileşme gösteriyorsa TPC iyi bir seçenektir. TPT'yi ise yalnızca dış etkenler sizi buna zorluyorsa kullanın; örneğin mevcut bir veritabanının şeması zaten bu biçimdeyse.
Stratejiyi sonradan değiştirmek bir şema değişikliğidir: tablolar birleşir ya da bölünür ve veri taşınır. Bu yüzden kararı model büyümeden ve üretime çıkmadan vermek, değişiklik gerekiyorsa üretilen migration'ı dikkatle incelemek gerekir. Migration iş akışı için migration rehberine bakın.
Kalıtım ve table splitting, complex types
Aynı tabloyu paylaşan (table splitting) dependent türler de bir kalıtım hiyerarşisine sahip olabilir, ama belgelere göre bazı sınırlamalar vardır: dependent tür TPC kullanamaz, çünkü türetilmiş türler aynı tabloya eşlenemez; TPT kullanabilir ama yalnızca kök tür table splitting yapabilir; principal tür TPC kullanıyorsa yalnızca alt türü olmayan türler table splitting kullanabilir. Owned entity türleri ise kalıtım hiyerarşisine sahip olamaz. Belgeler, complex type ve JSON kolonlarının TPT ve TPC kullanan entity'lerde kullanılabilmesini EF Core 11 ile gelen bir özellik olarak anlatıyor; EF Core 11 geliştirme aşamasında olduğu için bunu EF Core 10 projelerinde beklemeyin. Ayrıntılar complex types yazısında.
Sık yapılan hatalar

- Alt türlerin kendiliğinden ekleneceğini sanmak. EF hiyerarşiyi taramaz; her türü
DbSetveyaEntity<T>()ile modele ekleyin. - Discriminator değeri eşlenmemiş satırlar. Tabloya başka bir uygulama farklı discriminator değerleri yazıyorsa sorgular hata verir;
IsComplete(false)kullanın ya da değerleri eşleyin. - Ortak kolonda cast ile sorgulamak. Kardeş türlerin paylaştığı bir kolonda
asile cast, discriminator filtresini eklemez; açık tür kontrolü yazın. - SQLite ile TPC ve tamsayı anahtar. Desteklenmez; GUID ya da istemci tarafında üretilen anahtar kullanın.
- TPC'de ayrı identity kolonlarına güvenmek. Her tablonun kendi identity'si çakışan anahtarlar üretir; sequence ya da çakışmayan başlangıç ve artış değerleri gerekir.
- Şema temiz görünsün diye TPT seçmek. Belgeler TPT'nin çoğu durumda en yavaş seçenek olduğunu söylüyor; ölçmeden karar vermeyin.
Sık Sorulan Sorular
Hiçbir şey yapılandırmazsam hangi strateji kullanılır?
TPH. Hiyerarşideki türleri modele eklediğinizde EF bunları tek tabloya eşler ve örtük bir Discriminator kolonu oluşturur.
Discriminator kolonunu kaldırabilir miyim?
TPH'de satırın türünü belirleyen tek bilgi discriminator olduğu için kaldırılamaz; adını, tipini ve değerlerini değiştirebilir ya da bir C# özelliğine eşleyebilirsiniz. Discriminator istemiyorsanız TPT veya TPC kullanmanız gerekir, çünkü orada tür bilgisi tablonun kendisidir.
TPC neden SQL Server'da sequence oluşturuyor?
Çünkü hiyerarşideki bütün entity'lerin türleri farklı olsa bile benzersiz anahtar değerine sahip olması gerekir ve TPC'de anahtarı üretecek ortak bir tablo yoktur. Tek bir sequence, bütün tablolara çakışmayan değerler verir.
Abstract temel tür için tablo oluşur mu?
TPC'de oluşmaz, çünkü abstract türün örneği hiçbir zaman kaydedilmez. TPH'de bütün hiyerarşi tek tabloda olduğu için zaten ayrı tablo söz konusu değildir; TPT'de ise her türün kendi tablosu vardır.
Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.
İlgili içerikler: EF Core öğrenme rehberi · Model yapılandırma · Değer üretimi ve sequence · EF Core performans · Complex types ve owned entity · Migration rehberi