EFEF Core Handbook

İLERİ

Kalıtım (Inheritance) — TPH / TPT / TPC

Ortak bir base class'tan türeyen entity'leri veritabanında nasıl saklayacağını belirler. Üç strateji var: TPH (tek tablo, en hızlı), TPT (her tip ayrı tablo, normalize), TPC (her somut tip ayrı tablo, birleşik sorgu yok).

Veritabanı sağlayıcısı Bu sayfadaki eşleşen örnekleri seçilen sağlayıcıya göre gösterir.

TPH — Table Per Hierarchy

Ne zaman: Tipler arasındaki fark az, genellikle hepsini birlikte sorguluyorsun, performans önceliğin.

TPH — Tek tablo, discriminator sütunu:

Payments int IdPK decimal Amount datetime2 PaidAt int OrderIdFK nvarchar PaymentTypeDiscriminator nvarchar Last4Digitsnullable — credit_card nvarchar CardHoldernullable — credit_card nvarchar Ibannullable — bank_transfer nvarchar SenderNamenullable — bank_transfer nvarchar CourierNamenullable — cash_on_delivery
// --- Entity'ler ---

public abstract class Payment               // soyut taban
{
    public int      Id      { get; set; }
    public decimal  Amount  { get; set; }
    public DateTime PaidAt  { get; set; }
    public int      OrderId { get; set; }
}

public class CreditCardPayment : Payment
{
    public string Last4Digits { get; set; }  // sadece kredi kartında
    public string CardHolder  { get; set; }
}

public class BankTransferPayment : Payment
{
    public string Iban       { get; set; }   // sadece havalede
    public string SenderName { get; set; }
}

public class CashOnDeliveryPayment : Payment
{
    public string CourierName { get; set; }  // sadece kapıda ödemede
}

// --- Config ---

public class PaymentConfiguration : IEntityTypeConfiguration<Payment>
{
    public void Configure(EntityTypeBuilder<Payment> builder)
    {
        builder.ToTable("Payments");         // tek tablo — TPH varsayılan
        builder.HasKey(p => p.Id);
        builder.Property(p => p.Amount).HasPrecision(18, 2);

        builder.HasDiscriminator<string>("PaymentType")
               .HasValue<CreditCardPayment>    ("credit_card")
               .HasValue<BankTransferPayment>  ("bank_transfer")
               .HasValue<CashOnDeliveryPayment>("cash_on_delivery");
    }
}

Oluşan SQL:

CREATE TABLE [Payments] (
    [Id]          INT           IDENTITY(1,1) NOT NULL,
    [Amount]      DECIMAL(18,2) NOT NULL,
    [PaidAt]      DATETIME2     NOT NULL,
    [OrderId]     INT           NOT NULL,
    [PaymentType] NVARCHAR(MAX) NOT NULL,       -- Discriminator
    -- CreditCardPayment sütunları
    [Last4Digits] NVARCHAR(MAX) NULL,
    [CardHolder]  NVARCHAR(MAX) NULL,
    -- BankTransferPayment sütunları
    [Iban]        NVARCHAR(MAX) NULL,
    [SenderName]  NVARCHAR(MAX) NULL,
    -- CashOnDeliveryPayment sütunları
    [CourierName]  NVARCHAR(MAX) NULL,
    CONSTRAINT [PK_Payments] PRIMARY KEY CLUSTERED ([Id])
);
CREATE TABLE payments (
    id           INTEGER GENERATED ALWAYS AS IDENTITY,
    amount       NUMERIC(18,2) NOT NULL,
    paid_at      TIMESTAMPTZ   NOT NULL,
    order_id     INTEGER       NOT NULL,
    payment_type TEXT          NOT NULL,       -- Discriminator
    -- credit_card sütunları
    last4_digits TEXT NULL,
    card_holder  TEXT NULL,
    -- bank_transfer sütunları
    iban         TEXT NULL,
    sender_name  TEXT NULL,
    -- cash_on_delivery sütunları
    courier_name TEXT NULL,
    CONSTRAINT pk_payments PRIMARY KEY (id)
);

Örnek veri — Payments tablosu (TPH):

Id Amount PaidAt OrderId PaymentType Last4Digits CardHolder Iban SenderName CourierName
1 84999.99 2025-01-15 1 credit_card 4532 Ahmet Y. NULL NULL NULL
2 12500.00 2025-01-16 2 bank_transfer NULL NULL TR12 0001... Elif D. NULL
3 3499.00 2025-01-17 3 cash_on_delivery NULL NULL NULL NULL Mehmet K.

Her satırda sadece kendi tipine ait sütunlar dolu, diğerleri NULL.

Kullanımda:

// Tüm ödemeleri çek (tek sorgu, join yok)
var tumOdemeler = context.Payments.ToList();

// Sadece kredi kartı ödemelerini filtrele
var kartlar = context.Payments.OfType<CreditCardPayment>().ToList();
-- OfType<CreditCardPayment>() → SQL:
SELECT [p].[Id], [p].[Amount], [p].[PaidAt], [p].[OrderId],
       [p].[Last4Digits], [p].[CardHolder]
FROM [Payments] AS [p]
WHERE [p].[PaymentType] = N'credit_card';
-- OfType<CreditCardPayment>() → SQL:
SELECT p.id, p.amount, p.paid_at, p.order_id,
       p.last4_digits, p.card_holder
FROM payments AS p
WHERE p.payment_type = 'credit_card';

TPT — Table Per Type

Ne zaman: Null dolu sütun istemiyorsun, DB normalize olsun, tipler arası fark büyük.

TPT — Her tip ayrı tablo, FK ile bağlı:

Employees int IdPK nvarchar FullName nvarchar Email decimal Salary Managers int IdPK,FK decimal Budget int TeamSize Developers int IdPK,FK nvarchar Speciality int SeniorityLevel is a is a
// --- Entity'ler ---

public class Employee               // ortak alanlar
{
    public int     Id       { get; set; }
    public string  FullName { get; set; }
    public string  Email    { get; set; }
    public decimal Salary   { get; set; }
}

public class Manager : Employee
{
    public decimal Budget   { get; set; }   // yöneticiye özgü
    public int     TeamSize { get; set; }
}

public class Developer : Employee
{
    public string Speciality    { get; set; }   // yazılımcıya özgü
    public int    SeniorityLevel { get; set; }
}

// --- Config ---

public class EmployeeConfiguration : IEntityTypeConfiguration<Employee>
{
    public void Configure(EntityTypeBuilder<Employee> builder)
    {
        builder.UseTptMappingStrategy();
        builder.ToTable("Employees");
        builder.HasKey(e => e.Id);
        builder.Property(e => e.FullName).IsRequired().HasMaxLength(150);
        builder.Property(e => e.Email)   .IsRequired().HasMaxLength(200);
        builder.Property(e => e.Salary)  .HasPrecision(18, 2);
    }
}

public class ManagerConfiguration : IEntityTypeConfiguration<Manager>
{
    public void Configure(EntityTypeBuilder<Manager> builder)
    {
        builder.ToTable("Managers");
        builder.Property(m => m.Budget).HasPrecision(18, 2);
    }
}

public class DeveloperConfiguration : IEntityTypeConfiguration<Developer>
{
    public void Configure(EntityTypeBuilder<Developer> builder)
    {
        builder.ToTable("Developers");
        builder.Property(d => d.Speciality).HasMaxLength(50);
    }
}

Oluşan SQL:

CREATE TABLE [Employees] (
    [Id]       INT           IDENTITY(1,1) NOT NULL,
    [FullName] NVARCHAR(150) NOT NULL,
    [Email]    NVARCHAR(200) NOT NULL,
    [Salary]   DECIMAL(18,2) NOT NULL,
    CONSTRAINT [PK_Employees] PRIMARY KEY CLUSTERED ([Id])
);

CREATE TABLE [Managers] (
    [Id]       INT           NOT NULL,
    [Budget]   DECIMAL(18,2) NOT NULL,
    [TeamSize] INT           NOT NULL,
    CONSTRAINT [PK_Managers] PRIMARY KEY CLUSTERED ([Id]),
    CONSTRAINT [FK_Managers_Employees] FOREIGN KEY ([Id]) REFERENCES [Employees]([Id]) ON DELETE CASCADE
);

CREATE TABLE [Developers] (
    [Id]             INT          NOT NULL,
    [Speciality]     NVARCHAR(50) NULL,
    [SeniorityLevel] INT          NOT NULL,
    CONSTRAINT [PK_Developers] PRIMARY KEY CLUSTERED ([Id]),
    CONSTRAINT [FK_Developers_Employees] FOREIGN KEY ([Id]) REFERENCES [Employees]([Id]) ON DELETE CASCADE
);
CREATE TABLE employees (
    id        INTEGER GENERATED ALWAYS AS IDENTITY,
    full_name VARCHAR(150) NOT NULL,
    email     VARCHAR(200) NOT NULL,
    salary    NUMERIC(18,2) NOT NULL,
    CONSTRAINT pk_employees PRIMARY KEY (id)
);

CREATE TABLE managers (
    id        INTEGER NOT NULL,
    budget    NUMERIC(18,2) NOT NULL,
    team_size INTEGER NOT NULL,
    CONSTRAINT pk_managers PRIMARY KEY (id),
    CONSTRAINT fk_managers_employees FOREIGN KEY (id) REFERENCES employees(id) ON DELETE CASCADE
);

CREATE TABLE developers (
    id              INTEGER NOT NULL,
    speciality      VARCHAR(50) NULL,
    seniority_level INTEGER NOT NULL,
    CONSTRAINT pk_developers PRIMARY KEY (id),
    CONSTRAINT fk_developers_employees FOREIGN KEY (id) REFERENCES employees(id) ON DELETE CASCADE
);

Örnek veri:

Employees:

Id FullName Email Salary
1 Ahmet Yılmaz [email protected] 45000.00
2 Elif Demir [email protected] 65000.00
3 Mert Kaya [email protected] 38000.00

Managers:

Id Budget TeamSize
2 500000.00 8

Developers:

Id Speciality SeniorityLevel
1 Backend 3
3 Frontend 1

TPC — Table Per Concrete Type

Ne zaman: Her tip yüksek hacimli veri üretiyor, join maliyeti kabul edilemez, tipler birbirinden bağımsız sorgulanıyor.

TPC — Tamamen bağımsız tablolar:

EmailNotifications int IdPK Seq nvarchar UserId nvarchar Message datetime2 SentAt bit IsRead nvarchar ToAddress nvarchar Subject SmsNotifications int IdPK Seq nvarchar UserId nvarchar Message datetime2 SentAt bit IsRead nvarchar PhoneNumber PushNotifications int IdPK Seq nvarchar UserId nvarchar Message datetime2 SentAt bit IsRead nvarchar DeviceToken nvarchar Platform Ortak tablo yok — her tip tamamen bağımsız Shared Sequence ile Id çakışması önlenir
// --- Entity'ler ---

public abstract class Notification      // soyut — DB'de tablosu olmaz
{
    public int      Id      { get; set; }
    public string   UserId  { get; set; }
    public string   Message { get; set; }
    public DateTime SentAt  { get; set; }
    public bool     IsRead  { get; set; }
}

public class EmailNotification : Notification
{
    public string ToAddress { get; set; }
    public string Subject   { get; set; }
}

public class SmsNotification : Notification
{
    public string PhoneNumber { get; set; }
}

public class PushNotification : Notification
{
    public string DeviceToken { get; set; }
    public string Platform    { get; set; }   // "iOS", "Android"
}

// --- Config ---

public class NotificationConfiguration : IEntityTypeConfiguration<Notification>
{
    public void Configure(EntityTypeBuilder<Notification> builder)
    {
        builder.UseTpcMappingStrategy();
        // TPC'de IDENTITY çakışır — sequence kullan
        builder.Property(n => n.Id).UseHiLo("NotificationSequence");
        builder.Property(n => n.UserId) .IsRequired().HasMaxLength(50);
        builder.Property(n => n.Message).IsRequired().HasMaxLength(500);
    }
}

public class EmailNotificationConfiguration : IEntityTypeConfiguration<EmailNotification>
{
    public void Configure(EntityTypeBuilder<EmailNotification> builder)
    {
        builder.ToTable("EmailNotifications");
        builder.Property(e => e.ToAddress).IsRequired().HasMaxLength(200);
        builder.Property(e => e.Subject)  .IsRequired().HasMaxLength(300);
    }
}

public class SmsNotificationConfiguration : IEntityTypeConfiguration<SmsNotification>
{
    public void Configure(EntityTypeBuilder<SmsNotification> builder)
    {
        builder.ToTable("SmsNotifications");
        builder.Property(s => s.PhoneNumber).IsRequired().HasMaxLength(20);
    }
}

public class PushNotificationConfiguration : IEntityTypeConfiguration<PushNotification>
{
    public void Configure(EntityTypeBuilder<PushNotification> builder)
    {
        builder.ToTable("PushNotifications");
        builder.Property(p => p.DeviceToken).IsRequired();
        builder.Property(p => p.Platform)   .HasMaxLength(10);
    }
}

Oluşan SQL:

-- Sequence (tüm tablolarda benzersiz Id üretmek için)
CREATE SEQUENCE [NotificationSequence] AS INT START WITH 1 INCREMENT BY 10;

CREATE TABLE [EmailNotifications] (
    [Id]        INT           NOT NULL,
    [UserId]    NVARCHAR(50)  NOT NULL,
    [Message]   NVARCHAR(500) NOT NULL,
    [SentAt]    DATETIME2     NOT NULL,
    [IsRead]    BIT           NOT NULL,
    [ToAddress] NVARCHAR(200) NOT NULL,
    [Subject]   NVARCHAR(300) NOT NULL,
    CONSTRAINT [PK_EmailNotifications] PRIMARY KEY CLUSTERED ([Id])
);

CREATE TABLE [SmsNotifications] (
    [Id]          INT           NOT NULL,
    [UserId]      NVARCHAR(50)  NOT NULL,
    [Message]     NVARCHAR(500) NOT NULL,
    [SentAt]      DATETIME2     NOT NULL,
    [IsRead]      BIT           NOT NULL,
    [PhoneNumber] NVARCHAR(20)  NOT NULL,
    CONSTRAINT [PK_SmsNotifications] PRIMARY KEY CLUSTERED ([Id])
);

CREATE TABLE [PushNotifications] (
    [Id]          INT           NOT NULL,
    [UserId]      NVARCHAR(50)  NOT NULL,
    [Message]     NVARCHAR(500) NOT NULL,
    [SentAt]      DATETIME2     NOT NULL,
    [IsRead]      BIT           NOT NULL,
    [DeviceToken] NVARCHAR(MAX) NOT NULL,
    [Platform]    NVARCHAR(10)  NULL,
    CONSTRAINT [PK_PushNotifications] PRIMARY KEY CLUSTERED ([Id])
);
-- Sequence (tüm tablolarda benzersiz Id üretmek için)
CREATE SEQUENCE notification_sequence START WITH 1 INCREMENT BY 10;

CREATE TABLE email_notifications (
    id         INTEGER NOT NULL DEFAULT nextval('notification_sequence'),
    user_id    VARCHAR(50)  NOT NULL,
    message    VARCHAR(500) NOT NULL,
    sent_at    TIMESTAMPTZ  NOT NULL,
    is_read    BOOLEAN      NOT NULL DEFAULT FALSE,
    to_address VARCHAR(200) NOT NULL,
    subject    VARCHAR(300) NOT NULL,
    CONSTRAINT pk_email_notifications PRIMARY KEY (id)
);

CREATE TABLE sms_notifications (
    id           INTEGER NOT NULL DEFAULT nextval('notification_sequence'),
    user_id      VARCHAR(50)  NOT NULL,
    message      VARCHAR(500) NOT NULL,
    sent_at      TIMESTAMPTZ  NOT NULL,
    is_read      BOOLEAN      NOT NULL DEFAULT FALSE,
    phone_number VARCHAR(20)  NOT NULL,
    CONSTRAINT pk_sms_notifications PRIMARY KEY (id)
);

CREATE TABLE push_notifications (
    id           INTEGER NOT NULL DEFAULT nextval('notification_sequence'),
    user_id      VARCHAR(50)  NOT NULL,
    message      VARCHAR(500) NOT NULL,
    sent_at      TIMESTAMPTZ  NOT NULL,
    is_read      BOOLEAN      NOT NULL DEFAULT FALSE,
    device_token TEXT         NOT NULL,
    platform     VARCHAR(10)  NULL,
    CONSTRAINT pk_push_notifications PRIMARY KEY (id)
);

Örnek veri:

EmailNotifications:

Id UserId Message SentAt IsRead ToAddress Subject
1 user-1 Siparişiniz kargoya verildi 2025-01-15 10:00 1 [email protected] Kargo Bildirimi
11 user-2 Faturanız hazır 2025-01-16 14:30 0 [email protected] Fatura

SmsNotifications:

Id UserId Message SentAt IsRead PhoneNumber
21 user-1 Doğrulama kodunuz: 4829 2025-01-15 10:01 1 +905551234567

Id'ler sequence ile üretildiği için tablolar arası çakışma yok (1, 11, 21...).


Karşılaştırma Tablosu

Durum Seçim
Tipler arasındaki fark az, sorgular genelde tüm hiyerarşiyi kapsıyor TPH
DB normalize olsun, null dolu sütun istemiyorum TPT
Her tip yüksek hacimli, join maliyeti kabul edilemez TPC
Derived type'ları çoğunlukla ayrı ayrı sorguluyorum TPC
Strateji Tablo sayısı Join Null sütun Performans
TPH 1 Yok Çok En hızlı
TPT N FK join Yok Yavaş
TPC N (concrete) Yok Yok Hızlı

PostgreSQL'de Inheritance:

  • PostgreSQL'in kendi TABLE INHERITANCE özelliği var (INHERITS keyword) ama EF Core bunu kullanmaz
  • EF Core TPH/TPT/TPC stratejileri PostgreSQL'de aynı şekilde çalışır (standart tablolar)
  • TPH'da discriminator: VARCHAR (SQL Server'daki NVARCHAR yerine)
  • TPC'de sequence paylaşımı: PostgreSQL native sequence ile sorunsuz çalışır
  • Table Partitioning (PostgreSQL 10+): Büyük tablolarda PARTITION BY RANGE/LIST/HASH ile fiziksel bölümleme yapılabilir — EF bunu bilmez ama DBA tarafında uygulanır
-- PostgreSQL native partitioning (DBA seviyesinde):
CREATE TABLE notifications (
    id         INT GENERATED ALWAYS AS IDENTITY,
    user_id    TEXT NOT NULL,
    message    TEXT NOT NULL,
    type       TEXT NOT NULL,        -- 'email', 'sms', 'push'
    sent_at    TIMESTAMPTZ NOT NULL
) PARTITION BY LIST (type);

CREATE TABLE notifications_email PARTITION OF notifications FOR VALUES IN ('email');
CREATE TABLE notifications_sms   PARTITION OF notifications FOR VALUES IN ('sms');
CREATE TABLE notifications_push  PARTITION OF notifications FOR VALUES IN ('push');
-- EF bunu tek tablo olarak görür, PG arka planda doğru partition'a yönlendirir