EFEF Core Handbook

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.

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

Ö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)

Users int IdPK nvarchar Name UserProfiles int IdPK nvarchar Bio int UserIdFK UQ 1 0..1 has
// --- 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)

Categories int IdPK nvarchar Name Products int IdPK nvarchar Name int CategoryIdFK 1 * contains
// --- 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+)

Students int IdPK nvarchar Name Courses int IdPK nvarchar Title CourseStudent int CoursesIdFK int StudentsIdFK * * enrolls
// --- 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ı:

Students int IdPK nvarchar Name Courses int IdPK nvarchar Title StudentCourses int StudentIdPK,FK int CourseIdPK,FK datetime2 EnrolledAt 1 1 * *
// --- 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ş

EnrolledAt sayesinde 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 silinsin
  • Restrict → Kategori silinemesin eğer altında ürün varsa
  • SetNull → 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 RESTRICT ile NO ACTION pratikte 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 RESTRICT vs NO ACTION farkı 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;
");