EF Core'da one-to-many (bire çok) ilişki, bir entity'nin herhangi sayıda başka entity ile ilişkili olduğu durumdur: bir Blog birçok Post içerebilir, ama her Post yalnızca bir Blog'a aittir. Entity Framework Core'da bu ilişki principal taraftan HasMany(...).WithOne(...) ya da dependent taraftan HasOne(...).WithMany(...) ile yapılandırılır; foreign key her zaman "çok" tarafındadır.

Bu yazıda resmî belgelerdeki örnek sırasını izleyerek required ve optional ilişkiyi, shadow foreign key'i, tek yönlü (unidirectional) ilişkileri, alternatif ve bileşik anahtarları, cascade delete'i kapatmayı ve self-referencing ilişkiyi anlatıyoruz. Principal, dependent ve navigation gibi temel terimler için önce EF Core ilişkiler yazısına, serinin geri kalanı için EF Core öğrenme rehberi sayfasına bakabilirsiniz.

Required one-to-many

Required one-to-many

Belgelerdeki temel örnekte principal Blog yazılarını bir collection navigation'da tutar; dependent Post ise nullable olmayan bir BlogId foreign key'i ve bloga giden bir reference navigation taşır:

csharp
// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>(); // dependent'ları tutan collection navigation
}

// Dependent (child)
public class Post
{
    public int Id { get; set; }
    public int BlogId { get; set; }          // required foreign key
    public Blog Blog { get; set; } = null!;  // principal'a giden required reference navigation
}

Bir one-to-many ilişki şu parçalardan oluşur: principal taraftaki birincil veya alternatif anahtar (Blog.Id), dependent taraftaki foreign key (Post.BlogId) ve isteğe bağlı olarak iki navigation (Blog.Posts ile Post.Blog). Foreign key nullable olmadığı için ilişki required'dır: her yazının bir bloga bağlanması zorunludur.

Bu model hiç yapılandırma yazmadan konvansiyonla keşfedilir: Blog principal, Post dependent olur; Post.BlogId foreign key olarak bulunur ve nullable olmadığı için ilişki required sayılır. Konvansiyonun yetmediği durumlarda ya da niyetinizi açıkça göstermek için aynı ilişkiyi şöyle yazabilirsiniz:

csharp
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne(e => e.Blog)
        .HasForeignKey(e => e.BlogId)
        .IsRequired();
}

Aynı yapılandırmayı dependent taraftan başlayarak da yazabilirsiniz; belgelere göre iki yazım arasında hiçbir fark yoktur, tamamen aynı modeli üretirler:

csharp
modelBuilder.Entity<Post>()
    .HasOne(e => e.Blog)
    .WithMany(e => e.Posts)
    .HasForeignKey(e => e.BlogId)
    .IsRequired();

Nullable reference types açıkken dikkat edilecek bir kural var: foreign key nullable ise reference navigation da nullable olmalıdır. Foreign key nullable değilse navigation nullable olabilir de olmayabilir de. Örnekteki = null!; yazımı derleyiciye bunun bilinçli olduğunu söyler; EF tam yüklenmiş bir ilişkide bu özelliği doldurur. Required ilişkinin yalnızca dependent tarafı bağladığını da unutmayın: hiç yazısı olmayan bir blog yine geçerlidir; "her blogun en az bir yazısı olmalı" gibi bir kural ancak iş mantığında uygulanabilir.

Optional one-to-many

Foreign key'i ve dependent'tan principal'a giden navigation'ı nullable yaptığınızda ilişki optional olur; bir yazı herhangi bir bloga bağlı olmadan da var olabilir:

csharp
public class Post
{
    public int Id { get; set; }
    public int? BlogId { get; set; }  // optional foreign key
    public Blog? Blog { get; set; }   // optional reference navigation
}

Bu da konvansiyonla keşfedilir. Açıkça yazmak isterseniz tek fark IsRequired(false) çağrısıdır. Optional ilişkilerde konvansiyonel silme davranışı da farklıdır: required ilişkiler cascade delete ile, optional ilişkiler cascade olmadan yapılandırılır. Bir blog silindiğinde optional ilişkideki yazıların ne olacağı cascade delete yazısında ayrıntılı olarak anlatılıyor.

Shadow foreign key ile one-to-many

Foreign key bir veritabanı ayrıntısıdır ve ilişkiyi tamamen nesne yönelimli biçimde kullanıyorsanız sınıfta görmek istemeyebilirsiniz. Post sınıfından BlogId özelliğini kaldırdığınızda EF, BlogId adında bir shadow foreign key oluşturur. Bu özelliğin nullable olup olmayacağı, nullable reference types açıksa navigation'ın nullable olmasına bağlıdır: Blog Blog required, Blog? Blog optional bir ilişki üretir (shadow özelliğin tipi sırasıyla int ve int? olur).

Nullable reference types kapalıysa durum değişir: shadow foreign key varsayılan olarak nullable oluşturulur, yani ilişki varsayılan olarak optional olur. Required istiyorsanız bunu söylemeniz gerekir:

csharp
modelBuilder.Entity<Blog>()
    .HasMany(e => e.Posts)
    .WithOne(e => e.Blog)
    .HasForeignKey("BlogId")
    .IsRequired();

Tek yönlü (unidirectional) one-to-many

Tek yönlü (unidirectional) one-to-many: Principal'a navigation yok, Dependent'lara navigation yok, Hiç navigation yok

İki navigation'dan birini kaldırdığınızda ilişki tek yönlü olur. Belgeler üç varyasyon gösterir ve her birinin yapılandırmasındaki küçük ama önemli farklar şunlardır.

Principal'a navigation yok

Post içinde BlogId var ama Blog navigation'ı yoksa ilişki yine konvansiyonla bulunur. Açıkça yazarken WithOne() argümansız çağrılır; bu, EF'e o yönde navigation olmadığını söyler. Yapılandırmaya navigation'ı olmayan taraftan başlarsanız karşı tarafın tipini generic parametreyle vermelisiniz:

csharp
modelBuilder.Entity<Blog>()
    .HasMany(e => e.Posts)
    .WithOne()
    .HasForeignKey(e => e.BlogId)
    .IsRequired();

// Navigation'ı olmayan taraftan başlarken:
modelBuilder.Entity<Post>()
    .HasOne<Blog>()
    .WithMany(e => e.Posts)
    .HasForeignKey(e => e.BlogId)
    .IsRequired();

Hem foreign key hem de dependent'taki navigation kaldırılırsa ilişki konvansiyonla optional olarak bulunur, çünkü kodda required olduğunu gösteren hiçbir şey kalmamıştır. Required istiyorsanız en azından HasMany(e => e.Posts).WithOne().IsRequired() yazmanız gerekir.

Dependent'lara navigation yok

Bu kez Blog sınıfında Posts koleksiyonu yoktur ama Post.Blog vardır. Bu da konvansiyonla keşfedilir. Açık yapılandırmada argümansız WithMany() kullanılır; navigation'ı olmayan Blog tarafından başlarsanız HasMany<Post>() yazarsınız:

csharp
modelBuilder.Entity<Post>()
    .HasOne(e => e.Blog)
    .WithMany()
    .HasForeignKey(e => e.BlogId)
    .IsRequired();

Hiç navigation yok

Bazen ilişkiyi modelde tutmak ama hiç navigation açmamak isteyebilirsiniz; böyle bir ilişki yalnızca foreign key değeri değiştirilerek yönetilir. Navigation olmadığı için EF iki türün ilişkili olduğunu tahmin edemez ve ilişki konvansiyonla keşfedilmez. En kısa yapılandırma modelBuilder.Entity<Blog>().HasMany<Post>().WithOne(); biçimindedir. Bu durumda bile Post.BlogId konvansiyonla foreign key olarak bulunur ve nullable olmadığı için ilişki required olur; nullable yaparsanız optional olur.

Alternatif anahtar ve bileşik foreign key

Foreign key varsayılan olarak principal'ın birincil anahtarını gösterir ve EF konvansiyonla her zaman birincil anahtara ilişki kurar. Foreign key'i başka bir özelliğe, örneğin Blog.AlternateId'ye bağlamak isterseniz bunu HasPrincipalKey ile açıkça söylemelisiniz; o özellik böylece principal'ın alternatif anahtarı olur:

csharp
modelBuilder.Entity<Blog>()
    .HasMany(e => e.Posts)
    .WithOne(e => e.Blog)
    .HasPrincipalKey(e => e.AlternateId)
    .HasForeignKey(e => e.BlogId)
    .IsRequired();

Principal'ın anahtarı birden çok özellikten oluşuyorsa (composite key) dependent'ın foreign key'i de aynı sayıda özellikten oluşmalıdır. İlişki konvansiyonla bulunur, ama bileşik anahtarın kendisi otomatik keşfedilmediği için HasKey(e => new { e.Id1, e.Id2 }) ile tanımlanmalıdır. Hepsini açıkça yazmak isterseniz belgelerdeki iç içe builder biçimini kullanabilirsiniz:

csharp
modelBuilder.Entity<Blog>(
    nestedBuilder =>
    {
        nestedBuilder.HasKey(e => new { e.Id1, e.Id2 });

        nestedBuilder.HasMany(e => e.Posts)
            .WithOne(e => e.Blog)
            .HasPrincipalKey(e => new { e.Id1, e.Id2 })
            .HasForeignKey(e => new { e.BlogId1, e.BlogId2 })
            .IsRequired();
    });

Cascade delete olmadan required ilişki

Konvansiyona göre required ilişkiler cascade delete ile yapılandırılır: principal silindiğinde dependent'lar da silinir, çünkü bir principal'a bağlı olmadan veritabanında var olamazlar. Bunun yerine, artık var olamayacak dependent satırları kendiliğinden silmek yerine hata alınmasını istiyorsanız silme davranışını değiştirebilirsiniz:

csharp
modelBuilder.Entity<Blog>()
    .HasMany(e => e.Posts)
    .WithOne(e => e.Blog)
    .OnDelete(DeleteBehavior.Restrict);

DeleteBehavior değerlerinin her birinin EF tarafında ve veritabanı tarafında ne yaptığı ayrı bir konudur; cascade delete yazısında karşılaştırmalı olarak anlatılıyor.

Self-referencing one-to-many

Principal ve dependent aynı tür olabilir. Belgelerdeki örnekte her Employee optional olarak bir yöneticiye bağlıdır ve kendisine bağlı çalışanların listesini tutar. Bu model de konvansiyonla keşfedilir:

csharp
public class Employee
{
    public int Id { get; set; }

    public int? ManagerId { get; set; }        // optional foreign key
    public Employee? Manager { get; set; }     // principal'a giden navigation
    public ICollection<Employee> Reports { get; } = new List<Employee>(); // dependent'lar
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Employee>()
        .HasOne(e => e.Manager)
        .WithMany(e => e.Reports)
        .HasForeignKey(e => e.ManagerId)
        .IsRequired(false);
}

Burada foreign key'in nullable olması mantıklıdır: en üstteki yöneticinin bir yöneticisi yoktur. Self-referencing ilişkide de diğer örneklerdeki kurallar aynen geçerlidir: foreign key nullable olduğu için navigation da nullable tanımlanmıştır ve ilişki bir kez, tek bir uçtan yapılandırılır.

Veritabanı tarafında ne oluşur?

One-to-many ilişki şemada üç iz bırakır. Birincisi foreign key kolonunun kendisidir; nullable olup olmaması ilişkinin required ya da optional olmasını yansıtır. Belgelere göre nullable bir foreign key özelliğini koruyup kolonu yine de NOT NULL yapmak mümkündür: ilişkiye IsRequired() eklemek ya da foreign key özelliğini Property(e => e.BlogId).IsRequired() ile işaretlemek aynı sonucu verir. İkincisi foreign key kısıtıdır; adı konvansiyonla FK_<dependent tip>_<principal tip>_<foreign key özelliği> kalıbındadır (bileşik foreign key'de özellik adları alt çizgiyle sıralanır) ve HasConstraintName ile değiştirilebilir. Bu ad yalnızca migration ile şema oluşturulurken kullanılır.

Üçüncüsü indekstir. EF foreign key için konvansiyonla sıradan, benzersiz olmayan bir indeks oluşturur ve required ile optional ilişkide aynı indeksi kullanır. Belgelerdeki SQL Server örnek çıktısı şöyledir:

sql
CREATE INDEX [IX_Post_BlogId] ON [Post] ([BlogId]);

Bileşik foreign key'de indeks bütün foreign key kolonlarını kapsar. Bu indeks bir blogun yazılarını getiren sorguları destekler; kendi sorgu kalıplarınıza göre ek indeks gerekip gerekmediğini index yazısında anlatılan yöntemlerle değerlendirebilirsiniz.

Varyasyonların özeti

DurumKonvansiyonla bulunur muGereken yapılandırma
FK ve iki navigationEvetYok; istenirse HasMany/WithOne
FK nullableEvet, optional olarakYok; açıkça IsRequired(false)
FK yok, navigation varEvet, shadow FK ileRequired için NRT veya IsRequired
Principal a navigation yokEvetAçık yazımda WithOne() argümansız
FK ve dependent navigation yokEvet, optional olarakRequired için IsRequired
Dependent lara navigation yokEvetAçık yazımda WithMany() argümansız
Hiç navigation yokHayırHasMany<Post>().WithOne()
Alternatif anahtarHayırHasPrincipalKey
Bileşik anahtarİlişki evet, anahtar hayırHasKey ile bileşik anahtar

Sık yapılan hatalar

Sık yapılan hatalar: Nullable reference types ile çelişen tanım., NRT kapalıyken shadow foreign key'in required sanılması., Navigation'ı olmayan taraftan başlarken tipi vermemek...
  • Nullable reference types ile çelişen tanım. int? BlogId ile Blog Blog (non-nullable) birlikte kullanılamaz; foreign key nullable ise navigation da nullable olmalıdır.
  • NRT kapalıyken shadow foreign key'in required sanılması. Bu durumda shadow foreign key nullable oluşur; ilişki optional'dır ve blogu olmayan yazılar kaydedilebilir. IsRequired() ekleyin.
  • Navigation'ı olmayan taraftan başlarken tipi vermemek. HasOne() yerine HasOne<Blog>(), HasMany() yerine HasMany<Post>() yazılmalıdır.
  • İlişkiyi iki uçtan iki kez yazmak. Bir kez, tek bir uçtan yapılandırın.
  • Bileşik anahtarı tanımlamadan bileşik foreign key beklemek. Bileşik anahtarlar konvansiyonla keşfedilmez; önce HasKey yazılmalıdır.

İlişki kurulduktan sonra verileri birlikte okumak için Include ve eager loading, yeni yazıları bir bloga eklemek ve ilişkiyi değiştirmek için SaveChanges yazısına bakabilirsiniz. Yeni bir Post nesnesini blog.Posts koleksiyonuna eklediğinizde foreign key değerini sizin yazmanıza gerek kalmaz; EF bunu ilişki üzerinden ayarlar.

Sık Sorulan Sorular

HasMany ile mi başlamalıyım, HasOne ile mi?

Fark etmez. Blog üzerinden HasMany(...).WithOne(...) ile Post üzerinden HasOne(...).WithMany(...) belgelere göre tam olarak aynı yapılandırmayı üretir. Kodunuzda hangisi daha okunaklıysa onu seçin, ama ilişkiyi yalnızca bir kez yazın.

Foreign key yazmazsam kolon yine oluşur mu?

Evet. EF bir shadow foreign key oluşturur; adı dependent'taki navigation varsa navigation adı ile principal key adının birleşimi (örneğin BlogId), navigation yoksa principal tip adı ile principal key adının birleşimidir. Tipi principal key'in tipidir.

Bir yazıyı başka bloga taşımak için ne yapmalıyım?

Navigation'ı (post.Blog = digerBlog) ya da foreign key değerini (post.BlogId = 5) değiştirmeniz yeterlidir; change tracker diğer tarafı buna göre düzeltir ve SaveChanges güncellemeyi gönderir. Bu sürecin ayrıntıları change tracker yazısında.

Hiç yazısı olmayan bir blog required ilişkiyi bozar mı?

Hayır. Required ilişki her yazının bir blogu olmasını zorunlu kılar, ama bir blogun en az bir yazısı olmasını zorunlu kılmaz. Böyle bir kuralı ne EF modelinde ne de standart SQL'de ifade edebilirsiniz; iş mantığında kontrol etmeniz gerekir.

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 · EF Core ilişkiler · One-to-one ilişki · Many-to-many ilişki · Cascade delete · Include ve eager loading