İ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).
TPH — Table Per Hierarchy
Ne zaman: Tipler arasındaki fark az, genellikle hepsini birlikte sorguluyorsun, performans önceliğin.
TPH — Tek tablo, discriminator sütunu:
// --- 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ı:
// --- 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 | 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:
// --- 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 (
INHERITSkeyword) 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'dakiNVARCHARyerine)- TPC'de sequence paylaşımı: PostgreSQL native sequence ile sorunsuz çalışır
- Table Partitioning (PostgreSQL 10+): Büyük tablolarda
PARTITION BY RANGE/LIST/HASHile 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