ORTA
İlişki Yapılandırması (Relationships)
Entity'ler arası bağlantıları (One-to-One, One-to-Many, Many-to-Many) Fluent API ile tanımlar. Temel kural: ilişki her zaman FK'nın bulunduğu entity'nin config'inde yazılır.
Önemli: EF Core ilişki çözümlemesi için sadece kendi model konfigürasyonuna bakar. Veritabanında fiziksel FK constraint olup olmaması sorgu üretimini etkilemez.
| Durum | EF JOIN/Include çalışır mı? | Veri bütünlüğü garantisi |
|---|---|---|
| EF config + DB FK | DB engeller | |
| EF config + DB FK | Orphan kayıt olabilir | |
| EF config + DB FK | Navigation yok | DB engeller |
One-to-One (Bire Bir)
// --- Entity'ler ---
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public UserProfile Profile { get; set; } // navigation
}
public class UserProfile
{
public int Id { get; set; }
public string Bio { get; set; }
public int UserId { get; set; } // ← FK burada, o yüzden ilişkiyi
public User User { get; set; } // UserProfileConfiguration yazar
}
// --- UserConfiguration: sadece User'ın kendi alanları, ilişki YOK ---
public class UserConfiguration : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> builder)
{
builder.ToTable("Users");
builder.HasKey(u => u.Id);
builder.Property(u => u.Name).IsRequired().HasMaxLength(100);
// ilişkiyi buraya YAZMA — FK bu tabloda değil
}
}
// --- UserProfileConfiguration: FK burada → ilişkiyi bu config yazar ---
public class UserProfileConfiguration : IEntityTypeConfiguration<UserProfile>
{
public void Configure(EntityTypeBuilder<UserProfile> builder)
{
builder.ToTable("UserProfiles");
builder.HasKey(p => p.Id);
builder.Property(p => p.Bio).HasMaxLength(500);
builder.HasOne(p => p.User) // UserProfile'ın bir User'ı var
.WithOne(u => u.Profile) // User'ın bir Profile'ı var
.HasForeignKey<UserProfile> // FK bu tabloda (UserProfile)
(p => p.UserId)
.OnDelete(DeleteBehavior.Cascade);
//
// ⚠️ 1-1'de HasForeignKey<T> generic ZORUNLU:
// EF iki taraftan hangisinin FK taşıdığını bilemez.
// FK zaten bu sınıfta (UserProfile) olduğu için <UserProfile> yazıyoruz.
}
}
Oluşan SQL:
CREATE TABLE [Users] (
[Id] INT IDENTITY(1,1) NOT NULL,
[Name] NVARCHAR(100) NOT NULL,
CONSTRAINT [PK_Users] PRIMARY KEY CLUSTERED ([Id])
);
CREATE TABLE [UserProfiles] (
[Id] INT IDENTITY(1,1) NOT NULL,
[Bio] NVARCHAR(500) NULL,
[UserId] INT NOT NULL,
CONSTRAINT [PK_UserProfiles] PRIMARY KEY CLUSTERED ([Id]),
CONSTRAINT [FK_UserProfiles_Users_UserId]
FOREIGN KEY ([UserId]) REFERENCES [Users]([Id]) ON DELETE CASCADE,
CONSTRAINT [IX_UserProfiles_UserId] UNIQUE ([UserId]) -- 1-1 garantisi
);
CREATE TABLE users (
id INTEGER GENERATED ALWAYS AS IDENTITY,
name VARCHAR(100) NOT NULL,
CONSTRAINT pk_users PRIMARY KEY (id)
);
CREATE TABLE user_profiles (
id INTEGER GENERATED ALWAYS AS IDENTITY,
bio VARCHAR(500) NULL,
user_id INTEGER NOT NULL,
CONSTRAINT pk_user_profiles PRIMARY KEY (id),
CONSTRAINT fk_user_profiles_users_user_id
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
CONSTRAINT ix_user_profiles_user_id UNIQUE (user_id) -- 1-1 garantisi
);
Örnek veri:
Users tablosu:
| Id | Name |
|---|---|
| 1 | Ahmet Yılmaz |
| 2 | Elif Demir |
UserProfiles tablosu:
| Id | Bio | UserId |
|---|---|---|
| 1 | Backend developer, 5 yıl deneyim | 1 |
| 2 | Full-stack, React & .NET | 2 |
UserId sütununda UNIQUE constraint var — aynı user'a iki profil atanamaz!
One-to-Many (Bire Çok)
// --- Entity'ler ---
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Product> Products { get; set; } // navigation
}
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public int CategoryId { get; set; } // ← FK burada, o yüzden ilişkiyi
public Category Category { get; set; } // ProductConfiguration yazar
}
// --- CategoryConfiguration: sadece Category'nin kendi alanları, ilişki YOK ---
public class CategoryConfiguration : IEntityTypeConfiguration<Category>
{
public void Configure(EntityTypeBuilder<Category> builder)
{
builder.ToTable("Categories");
builder.HasKey(c => c.Id);
builder.Property(c => c.Name).IsRequired().HasMaxLength(100);
// ilişkiyi buraya YAZMA — FK bu tabloda değil
}
}
// --- ProductConfiguration: FK burada → ilişkiyi bu config yazar ---
public class ProductConfiguration : IEntityTypeConfiguration<Product>
{
public void Configure(EntityTypeBuilder<Product> builder)
{
builder.ToTable("Products");
builder.HasKey(p => p.Id);
builder.Property(p => p.Name).IsRequired().HasMaxLength(200);
builder.HasOne(p => p.Category) // Product'ın bir Category'si var
.WithMany(c => c.Products) // Category'nin çok Product'ı var
.HasForeignKey(p => p.CategoryId) // FK bu tabloda (Product)
.OnDelete(DeleteBehavior.Restrict);
//
// ✅ 1-N'de generic gerek yok:
// builder zaten FK'nın bulunduğu taraf (Product), EF bilir.
}
}
Oluşan SQL:
CREATE TABLE [Categories] (
[Id] INT IDENTITY(1,1) NOT NULL,
[Name] NVARCHAR(100) NOT NULL,
CONSTRAINT [PK_Categories] PRIMARY KEY CLUSTERED ([Id])
);
CREATE TABLE [Products] (
[Id] INT IDENTITY(1,1) NOT NULL,
[Name] NVARCHAR(200) NOT NULL,
[CategoryId] INT NOT NULL,
CONSTRAINT [PK_Products] PRIMARY KEY CLUSTERED ([Id]),
CONSTRAINT [FK_Products_Categories_CategoryId]
FOREIGN KEY ([CategoryId]) REFERENCES [Categories]([Id]) ON DELETE NO ACTION
);
-- EF otomatik FK index oluşturur
CREATE NONCLUSTERED INDEX [IX_Products_CategoryId]
ON [Products] ([CategoryId]);
CREATE TABLE categories (
id INTEGER GENERATED ALWAYS AS IDENTITY,
name VARCHAR(100) NOT NULL,
CONSTRAINT pk_categories PRIMARY KEY (id)
);
CREATE TABLE products (
id INTEGER GENERATED ALWAYS AS IDENTITY,
name VARCHAR(200) NOT NULL,
category_id INTEGER NOT NULL,
CONSTRAINT pk_products PRIMARY KEY (id),
CONSTRAINT fk_products_categories_category_id
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE NO ACTION
);
-- EF otomatik FK index oluşturur
CREATE INDEX ix_products_category_id ON products (category_id);
Örnek veri:
Categories tablosu:
| Id | Name |
|---|---|
| 1 | Elektronik |
| 2 | Giyim |
| 3 | Kitap |
Products tablosu:
| Id | Name | CategoryId |
|---|---|---|
| 1 | MacBook Pro | 1 |
| 2 | iPhone 15 | 1 |
| 3 | Kış Montu | 2 |
| 4 | Clean Code | 3 |
| 5 | AirPods | 1 |
Many-to-Many — Basit (EF Core 5+)
// --- Entity'ler ---
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Course> Courses { get; set; } // navigation
}
public class Course
{
public int Id { get; set; }
public string Title { get; set; }
public ICollection<Student> Students { get; set; } // navigation
}
// --- StudentConfiguration / CourseConfiguration: kendi alanları ---
public class StudentConfiguration : IEntityTypeConfiguration<Student>
{
public void Configure(EntityTypeBuilder<Student> builder)
{
builder.ToTable("Students");
builder.HasKey(s => s.Id);
builder.Property(s => s.Name).IsRequired().HasMaxLength(100);
// ilişkiyi YAZMA — join tablo EF tarafından yönetilir
}
}
public class CourseConfiguration : IEntityTypeConfiguration<Course>
{
public void Configure(EntityTypeBuilder<Course> builder)
{
builder.ToTable("Courses");
builder.HasKey(c => c.Id);
builder.Property(c => c.Title).IsRequired().HasMaxLength(200);
// FK'lar EF'in otomatik oluşturduğu join tabloda olduğu için
// ilişkiyi bu iki config'den birinde, bir kez tanımlarız.
// Hangisinde olduğu fark etmez — burada Course'da tanımladık.
builder.HasMany(c => c.Students) // Course'un çok Student'ı var
.WithMany(s => s.Courses); // Student'ın da çok Course'u var
//
// EF otomatik join tablo oluşturur: "CourseStudent"
// İki config'e de YAZMA — bir kez yeterli, çakışır.
}
}
Oluşan SQL (EF otomatik üretir):
CREATE TABLE [Students] (
[Id] INT IDENTITY(1,1) NOT NULL,
[Name] NVARCHAR(100) NOT NULL,
CONSTRAINT [PK_Students] PRIMARY KEY CLUSTERED ([Id])
);
CREATE TABLE [Courses] (
[Id] INT IDENTITY(1,1) NOT NULL,
[Title] NVARCHAR(200) NOT NULL,
CONSTRAINT [PK_Courses] PRIMARY KEY CLUSTERED ([Id])
);
-- EF'in otomatik oluşturduğu join tablo
CREATE TABLE [CourseStudent] (
[CoursesId] INT NOT NULL,
[StudentsId] INT NOT NULL,
CONSTRAINT [PK_CourseStudent] PRIMARY KEY CLUSTERED ([CoursesId], [StudentsId]),
CONSTRAINT [FK_CourseStudent_Courses] FOREIGN KEY ([CoursesId]) REFERENCES [Courses]([Id]) ON DELETE CASCADE,
CONSTRAINT [FK_CourseStudent_Students] FOREIGN KEY ([StudentsId]) REFERENCES [Students]([Id]) ON DELETE CASCADE
);
CREATE TABLE students (
id INTEGER GENERATED ALWAYS AS IDENTITY,
name VARCHAR(100) NOT NULL,
CONSTRAINT pk_students PRIMARY KEY (id)
);
CREATE TABLE courses (
id INTEGER GENERATED ALWAYS AS IDENTITY,
title VARCHAR(200) NOT NULL,
CONSTRAINT pk_courses PRIMARY KEY (id)
);
-- EF'in otomatik oluşturduğu join tablo
CREATE TABLE course_student (
courses_id INTEGER NOT NULL,
students_id INTEGER NOT NULL,
CONSTRAINT pk_course_student PRIMARY KEY (courses_id, students_id),
CONSTRAINT fk_course_student_courses FOREIGN KEY (courses_id) REFERENCES courses(id) ON DELETE CASCADE,
CONSTRAINT fk_course_student_students FOREIGN KEY (students_id) REFERENCES students(id) ON DELETE CASCADE
);
Örnek veri:
Students:
| Id | Name |
|---|---|
| 1 | Ali Vural |
| 2 | Zeynep Kaya |
| 3 | Mert Özkan |
Courses:
| Id | Title |
|---|---|
| 1 | C# Temelleri |
| 2 | ASP.NET Core |
| 3 | Veritabanı Yönetimi |
CourseStudent (join tablo):
| CoursesId | StudentsId | Açıklama |
|---|---|---|
| 1 | 1 | Ali → C# Temelleri |
| 1 | 2 | Zeynep → C# Temelleri |
| 2 | 1 | Ali → ASP.NET Core |
| 2 | 3 | Mert → ASP.NET Core |
| 3 | 2 | Zeynep → Veritabanı |
Many-to-Many — Özel Join Entity ile
Join tabloya ekstra alan (ör.
EnrolledAt) eklemek istiyorsan bu yol gerekir.
ER Diyagramı:
// --- Entity'ler ---
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<StudentCourse> StudentCourses { get; set; }
}
public class Course
{
public int Id { get; set; }
public string Title { get; set; }
public ICollection<StudentCourse> StudentCourses { get; set; }
}
public class StudentCourse
{
public int StudentId { get; set; } // ← FK
public int CourseId { get; set; } // ← FK
public DateTime EnrolledAt { get; set; } // ekstra alan
public Student Student { get; set; }
public Course Course { get; set; }
}
// --- StudentConfiguration / CourseConfiguration: kendi alanları, ilişki YOK ---
public class StudentConfiguration : IEntityTypeConfiguration<Student>
{
public void Configure(EntityTypeBuilder<Student> builder)
{
builder.ToTable("Students");
builder.HasKey(s => s.Id);
builder.Property(s => s.Name).IsRequired().HasMaxLength(100);
// ilişkiyi buraya YAZMA — FK'lar StudentCourse'da
}
}
public class CourseConfiguration : IEntityTypeConfiguration<Course>
{
public void Configure(EntityTypeBuilder<Course> builder)
{
builder.ToTable("Courses");
builder.HasKey(c => c.Id);
builder.Property(c => c.Title).IsRequired().HasMaxLength(200);
// ilişkiyi buraya YAZMA — FK'lar StudentCourse'da
}
}
// --- StudentCourseConfiguration: her iki FK burada → ilişkileri bu config yazar ---
public class StudentCourseConfiguration : IEntityTypeConfiguration<StudentCourse>
{
public void Configure(EntityTypeBuilder<StudentCourse> builder)
{
builder.ToTable("StudentCourses");
builder.HasKey(sc => new { sc.StudentId, sc.CourseId }); // composite PK
builder.HasOne(sc => sc.Student) // bu kayıt bir Student'a ait
.WithMany(s => s.StudentCourses) // Student'ın çok StudentCourse'u var
.HasForeignKey(sc => sc.StudentId);
builder.HasOne(sc => sc.Course) // bu kayıt bir Course'a ait
.WithMany(c => c.StudentCourses) // Course'un çok StudentCourse'u var
.HasForeignKey(sc => sc.CourseId);
builder.Property(sc => sc.EnrolledAt)
.HasDefaultValueSql("GETUTCDATE()");
}
}
Oluşan SQL:
CREATE TABLE [StudentCourses] (
[StudentId] INT NOT NULL,
[CourseId] INT NOT NULL,
[EnrolledAt] DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
CONSTRAINT [PK_StudentCourses] PRIMARY KEY CLUSTERED ([StudentId], [CourseId]),
CONSTRAINT [FK_StudentCourses_Students] FOREIGN KEY ([StudentId]) REFERENCES [Students]([Id]) ON DELETE CASCADE,
CONSTRAINT [FK_StudentCourses_Courses] FOREIGN KEY ([CourseId]) REFERENCES [Courses]([Id]) ON DELETE CASCADE
);
CREATE TABLE student_courses (
student_id INTEGER NOT NULL,
course_id INTEGER NOT NULL,
enrolled_at TIMESTAMPTZ NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
CONSTRAINT pk_student_courses PRIMARY KEY (student_id, course_id),
CONSTRAINT fk_student_courses_students FOREIGN KEY (student_id) REFERENCES students(id) ON DELETE CASCADE,
CONSTRAINT fk_student_courses_courses FOREIGN KEY (course_id) REFERENCES courses(id) ON DELETE CASCADE
);
Örnek veri — StudentCourses:
| StudentId | CourseId | EnrolledAt | Açıklama |
|---|---|---|---|
| 1 | 1 | 2025-01-10 09:00:00 | Ali, C#'a Ocak'ta kayıt olmuş |
| 1 | 2 | 2025-03-01 14:30:00 | Ali, ASP.NET'e Mart'ta kayıt olmuş |
| 2 | 1 | 2025-01-12 11:00:00 | Zeynep, C#'a kayıt olmuş |
EnrolledAtsayesinde kimin ne zaman kayıt olduğunu biliyoruz — basit N-N'de bu bilgi kaybolurdu!
İlişki Özet Tablosu
| İlişki tipi | FK nerede | İlişkiyi kim yazar | Generic gerekli mi? |
|---|---|---|---|
| 1-1 | UserProfile.UserId |
UserProfileConfiguration |
Evet — EF hangi tarafın FK taşıdığını bilemez |
| 1-N | Product.CategoryId |
ProductConfiguration |
Hayır — builder zaten FK tarafı |
| N-N (otomatik) | EF yönetir (join tablo) | Herhangi birinde, bir kez | Hayır |
| N-N (join entity) | StudentCourse.StudentId + CourseId |
StudentCourseConfiguration |
Hayır |
DeleteBehavior Seçenekleri
| Değer | Açıklama | SQL Karşılığı |
|---|---|---|
Cascade |
Sahip silinince bağlı kayıtlar da silinir (varsayılan required) | ON DELETE CASCADE |
SetNull |
FK null yapılır (varsayılan optional) | ON DELETE SET NULL |
Restrict |
Silme engellenir | ON DELETE NO ACTION |
NoAction |
DB'ye bırakılır | ON DELETE NO ACTION |
ClientCascade |
Sadece takip edilen (tracked) nesnelerde cascade | — |
ClientSetNull |
Sadece tracked nesnelerde null (varsayılan optional, client) | — |
ClientNoAction |
EF hiçbir şey yapmaz | — |
Pratikte en çok kullanılan:
Cascade→ Sipariş silinince sipariş kalemleri de silinsinRestrict→ Kategori silinemesin eğer altında ürün varsaSetNull→ Yazar silinince kitaptaki AuthorId NULL olsun
SQL Server'da FK davranışları:
- FK constraint syntax:
REFERENCES,ON DELETE CASCADE— EF Fluent API aynı- SQL Server'da
ON DELETE RESTRICTileNO ACTIONpratikte aynı davranır (her ikisi de anında kontrol eder)- Deferrable FK desteklenmez — circular insert senaryolarında sıra önemlidir
-- SQL Server: FK her zaman anında kontrol eder (deferred yok)
-- Circular insert yapılamaz, sıralama zorunludur:
INSERT INTO Orders (Id) VALUES (1); -- Önce parent
INSERT INTO OrderItems (Id, OrderId) VALUES (10, 1); -- Sonra child
-- FK violation örneği:
INSERT INTO OrderItems (Id, OrderId) VALUES (20, 999); -- ❌ FK hata (Order 999 yok)
PostgreSQL İlişki Farkları:
- FK constraint syntax aynıdır (
REFERENCES,ON DELETE CASCADE) — EF Fluent API fark etmez- PostgreSQL'de
ON DELETE RESTRICTvsNO ACTIONfarkı var:
RESTRICT→ kontrol anında yapılır (deferred olamaz)NO ACTION→ transaction sonunda kontrol edilir (deferrable)- Deferrable FK → constraint kontrolünü transaction sonuna erteleyebilme özelliği
Deferrable FK Nedir? Normal FK constraint, INSERT/UPDATE anında hemen kontrol edilir. Deferrable FK ise kontrolü transaction'ın COMMIT anına erteler.
Ne işe yarar?
- Circular reference senaryolarında: A tablosu B'ye, B tablosu A'ya referans veriyorsa → hangi kaydı önce eklersin? Normal FK'da imkansız!
- Bulk data loading: Binlerce ilişkili kaydı sıra dert etmeden tek transaction'da ekle, COMMIT'te hepsi kontrol edilsin
- Data migration: Eski sistemden veri taşırken FK sırasını düşünmene gerek kalmaz
Modları:
| Mod | Açıklama |
|---|---|
NOT DEFERRABLE |
Varsayılan — her satır anında kontrol edilir |
DEFERRABLE INITIALLY IMMEDIATE |
Varsayılan olarak anında, ama SET CONSTRAINTS ... DEFERRED ile ertelenebilir |
DEFERRABLE INITIALLY DEFERRED |
Varsayılan olarak transaction COMMIT'te kontrol eder |
Dezavantaj: Hata geç fark edilir — COMMIT anında FK violation alırsın, hangi INSERT'in soruna yol açtığını bulmak zorlaşır.
-- PostgreSQL: Deferrable FK (SQL Server'da yok!)
ALTER TABLE order_items
ADD CONSTRAINT fk_order
FOREIGN KEY (order_id) REFERENCES orders(id)
ON DELETE CASCADE
DEFERRABLE INITIALLY DEFERRED; -- Transaction COMMIT'te kontrol eder
-- Bu, circular insert'lere izin verir:
BEGIN;
INSERT INTO orders (id, first_item_id) VALUES (1, 10); -- first_item_id henüz yok
INSERT INTO order_items (id, order_id) VALUES (10, 1); -- şimdi var
COMMIT; -- Her iki FK burada kontrol edilir → ✅
-- Geçici olarak deferrable yapma (INITIALLY IMMEDIATE olanlar için):
BEGIN;
SET CONSTRAINTS fk_order DEFERRED; -- Bu transaction için ertele
-- ... sırasız INSERT'ler ...
COMMIT; -- Burada kontrol edilir
// EF Core'da deferrable FK için Raw SQL migration gerekir:
migrationBuilder.Sql(@"
ALTER TABLE order_items
DROP CONSTRAINT fk_order_items_orders_order_id;
ALTER TABLE order_items
ADD CONSTRAINT fk_order_items_orders_order_id
FOREIGN KEY (order_id) REFERENCES orders(id)
ON DELETE CASCADE
DEFERRABLE INITIALLY DEFERRED;
");