一對多關聯
當單一實體與任意數目的其他實體相關聯時,會使用一對多關聯性。 例如, Blog
可以有許多相關聯的 Posts
,但每個都 Post
只與一個 Blog
相關聯。
本檔是以許多範例為結構。 這些範例會從常見案例開始,這也會介紹概念。 稍後的範例涵蓋較不常見的組態類型。 這裡的一個很好的方法是瞭解前幾個範例和概念,然後根據您的特定需求移至稍後的範例。 根據這種方法,我們將從簡單的「必要」和「選擇性」一對多關聯性開始。
提示
下列所有範例的程式代碼都可以在 OneToMany.cs中找到。
必要一對多
// Principal (parent)
public class Blog
{
public int Id { get; set; }
public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}
// Dependent (child)
public class Post
{
public int Id { get; set; }
public int BlogId { get; set; } // Required foreign key property
public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}
一對多關係是由下列專案所組成:
- 主體實體上的一或多個 主要或替代索引鍵 屬性,也就是關聯性的「一」端。 例如:
Blog.Id
。 - 相依實體上的一或多個 外鍵 屬性,也就是關聯性的「多」端。 例如:
Post.BlogId
。 - 選擇性地, 參考相依實體之主體實體上的集合導覽 。 例如:
Blog.Posts
。 - 或者, 參考主體實體之相依實體的參考導覽 。 例如:
Post.Blog
。
因此,針對此範例中的關聯性:
- 外鍵屬性
Post.BlogId
不可為 Null。 這會使關聯性「必要」,因為每個相依專案 (Post
) 都必須與某些主體 相關,Blog
因為其外鍵屬性必須設定為某些值。 - 這兩個實體都有指向關聯性另一端相關實體或實體的導覽。
注意
必要的關聯性可確保每個相依實體都必須與某些主體實體相關聯。 不過,主體實體一律可以存在,而不需要任何相依實體。 也就是說,必要的關聯性並不表示一律會有至少一個相依實體。 EF 模型沒有任何方法,也沒有任何標準方式在關係資料庫中,以確保主體與特定數目的相依性相關聯。 如果需要,則必須在應用程式(商業)邏輯中實作它。 如需詳細資訊,請參閱必要導覽。
提示
具有兩個巡覽的關聯性,一個從相依至主體,另一個從主體反轉為相依性,稱為雙向關聯性。
此關聯性是由 慣例所探索。 也就是說:
Blog
會探索為關聯性中的主體,並Post
探索為相依專案。Post.BlogId
探索為參考Blog.Id
主體主鍵之相依的外鍵。 因為不可為 Null,因此Post.BlogId
會視需要探索關聯性。Blog.Posts
探索為集合導覽。Post.Blog
探索為參考導覽。
重要
使用 C# 可為 Null 的參考型別時,如果外鍵屬性可為 Null,則參考導覽必須可為 Null。 如果外鍵屬性不可為 Null,則參考流覽可能是可為 Null 或不可為 Null。 在此情況下, Post.BlogId
是不可為 Null 且 Post.Blog
也是不可為 Null 的。 建 = null!;
構是用來將此標示為 C# 編譯程式的刻意,因為 EF 通常會設定 Blog
實例,而且對於完整載入的關聯性而言,它不可以是 Null。 如需詳細資訊,請參閱 使用可為 Null 的參考型 別。
如果慣例不會探索關聯性的導覽、外鍵或必要/選擇性本質,則可以明確地設定這些專案。 例如:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany(e => e.Posts)
.WithOne(e => e.Blog)
.HasForeignKey(e => e.BlogId)
.IsRequired();
}
在上述範例中,關聯性的組態會以 主體實體類型 () 開頭HasMany
,然後跟著這個與 WithOne
。Blog
與所有關聯性一樣,它完全相當於以相依實體類型 (Post
) 開頭,並使用 HasOne
後面接著 WithMany
。 例如:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasOne(e => e.Blog)
.WithMany(e => e.Posts)
.HasForeignKey(e => e.BlogId)
.IsRequired();
}
這兩個選項都比另一個選項都好;兩者都會產生完全相同的組態。
提示
絕對不需要設定關聯性兩次,一旦從主體開始,然後再從相依項目開始。 此外,嘗試個別設定關聯性的主體和相依部分通常無法運作。 選擇從一端或另一端設定每個關聯性,然後只撰寫組態程式代碼一次。
選擇性的一對多
// Principal (parent)
public class Blog
{
public int Id { get; set; }
public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}
// Dependent (child)
public class Post
{
public int Id { get; set; }
public int? BlogId { get; set; } // Optional foreign key property
public Blog? Blog { get; set; } // Optional reference navigation to principal
}
這與上一個範例相同,不同之處在於外鍵屬性和流覽至主體現在可為 Null。 這會使關聯性「選擇性」,因為相依性 (Post
) 可以存在 ,而不會 與任何主體 (Blog
) 相關。
重要
使用 C# 可為 Null 的參考型別時,如果外鍵屬性可為 Null,則參考導覽必須可為 Null。 在此情況下, Post.BlogId
可為 Null,因此 Post.Blog
也必須可為 Null。 如需詳細資訊,請參閱 使用可為 Null 的參考型 別。
和之前一樣,此關聯性是 依慣例探索的。 如果慣例不會探索關聯性的導覽、外鍵或必要/選擇性本質,則可以明確地設定這些專案。 例如:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany(e => e.Posts)
.WithOne(e => e.Blog)
.HasForeignKey(e => e.BlogId)
.IsRequired(false);
}
具有陰影外鍵的必要一對多
// Principal (parent)
public class Blog
{
public int Id { get; set; }
public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}
// Dependent (child)
public class Post
{
public int Id { get; set; }
public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}
在某些情況下,您可能不希望模型中的外鍵屬性,因為外鍵是資料庫中關聯性呈現方式的詳細數據,這在以純面向物件的方式使用關聯性時並不需要。 不過,如果要串行化實體,例如透過網路傳送,則當實體不在物件窗體中時,外鍵值可能會是保持關聯性資訊完好無損的有用方式。 因此,為了達到此目的,將外鍵屬性保留在 .NET 類型中通常是務實的。 外鍵屬性可以是私用的,這通常是很好的妥協,以避免公開外鍵,同時允許其值與實體一起移動。
在上述兩個範例之後,此範例會從相依實體類型中移除外鍵屬性。 因此,EF 會建立名為 BlogId
類型的int
陰影外鍵屬性。
此處 要注意的重點是,正在使用 C# 可為 Null 的參考型 別,因此參考導覽的 Null 性可用來判斷外鍵屬性是否可為 Null,因此關聯性為選擇性或必要。 如果未使用可為 Null 的參考型別,則陰影外鍵屬性預設會是可為 Null 的,因此關聯性預設為選擇性。 在此情況下,請使用 IsRequired
強制陰影外鍵屬性不可為 Null,並要求關聯性。
和之前一樣,此關聯性是 依慣例探索的。 如果慣例不會探索關聯性的導覽、外鍵或必要/選擇性本質,則可以明確地設定這些專案。 例如:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany(e => e.Posts)
.WithOne(e => e.Blog)
.HasForeignKey("BlogId")
.IsRequired();
}
具有陰影外鍵的選擇性一對多
// Principal (parent)
public class Blog
{
public int Id { get; set; }
public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}
// Dependent (child)
public class Post
{
public int Id { get; set; }
public Blog? Blog { get; set; } // Optional reference navigation to principal
}
如同上述範例,外鍵屬性已從相依實體類型中移除。 因此,EF 會建立名為 BlogId
類型的int?
陰影外鍵屬性。 不同於前一個範例,這次外鍵屬性會建立為可為 Null,因為 正在使用 C# 可為 Null 的參考型 別,而且相依實體類型的導覽是可為 Null 的。 這會讓關聯性成為選擇性的。
未使用 C# 可為 Null 的參考型別時,預設也會將外鍵屬性建立為可為 Null。 這表示自動建立陰影屬性的關聯性預設為選擇性。
和之前一樣,此關聯性是 依慣例探索的。 如果慣例不會探索關聯性的導覽、外鍵或必要/選擇性本質,則可以明確地設定這些專案。 例如:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany(e => e.Posts)
.WithOne(e => e.Blog)
.HasForeignKey("BlogId")
.IsRequired(false);
}
不流覽至主體的一對多
// Principal (parent)
public class Blog
{
public int Id { get; set; }
public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}
// Dependent (child)
public class Post
{
public int Id { get; set; }
public int BlogId { get; set; } // Required foreign key property
}
在此範例中,已重新導入外鍵屬性,但已移除相依專案的導覽。
提示
只有一個導覽的關聯性,一個從相依到主體,另一個則從主體到相依性(s),但不是兩者,稱為單向關聯性。
和之前一樣,此關聯性是 依慣例探索的。 如果慣例不會探索關聯性的導覽、外鍵或必要/選擇性本質,則可以明確地設定這些專案。 例如:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany(e => e.Posts)
.WithOne()
.HasForeignKey(e => e.BlogId)
.IsRequired();
}
請注意,對 WithOne
的呼叫沒有自變數。 這是告訴 EF 沒有從 Post
巡覽至 Blog
的方式。
如果組態從沒有導覽的實體開始,則必須使用泛型 HasOne<>()
呼叫明確指定關聯性另一端的實體類型。 例如:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasOne<Blog>()
.WithMany(e => e.Posts)
.HasForeignKey(e => e.BlogId)
.IsRequired();
}
不流覽至主體且具有陰影外鍵的一對多
// Principal (parent)
public class Blog
{
public int Id { get; set; }
public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}
// Dependent (child)
public class Post
{
public int Id { get; set; }
}
這個範例會藉由移除相依的外鍵屬性和巡覽,結合上述兩個範例。
此關聯性會 依慣例 探索為選擇性關聯性。 因為程式代碼中沒有任何專案可用來指出它應該是必要的,所以需要使用 一些最少的組態 IsRequired
來建立必要的關聯性。 例如:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany(e => e.Posts)
.WithOne()
.IsRequired();
}
更完整的組態可用來明確設定導覽和外鍵名稱,並視需要呼叫 IsRequired()
或 IsRequired(false)
。 例如:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany(e => e.Posts)
.WithOne()
.HasForeignKey("BlogId")
.IsRequired();
}
不流覽至相依項的一對多
// Principal (parent)
public class Blog
{
public int Id { get; set; }
}
// Dependent (child)
public class Post
{
public int Id { get; set; }
public int BlogId { get; set; } // Required foreign key property
public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}
上述兩個範例有從主體巡覽至相依專案,但不會從相依於主體巡覽。 在接下來的幾個範例中,會重新導入相依項目的導覽,而主體上的流覽會改為移除。
和之前一樣,此關聯性是 依慣例探索的。 如果慣例不會探索關聯性的導覽、外鍵或必要/選擇性本質,則可以明確地設定這些專案。 例如:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasOne(e => e.Blog)
.WithMany()
.HasForeignKey(e => e.BlogId)
.IsRequired();
}
再次請注意, WithMany()
呼叫 時沒有自變數,表示沒有此方向的流覽。
如果組態從沒有導覽的實體開始,則必須使用泛型 HasMany<>()
呼叫明確指定關聯性另一端的實體類型。 例如:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany<Post>()
.WithOne(e => e.Blog)
.HasForeignKey(e => e.BlogId)
.IsRequired();
}
無導覽的一對多
有時候,設定沒有導覽的關聯性會很有用。 這類關聯性只能藉由直接變更外鍵值來操作。
// Principal (parent)
public class Blog
{
public int Id { get; set; }
}
// Dependent (child)
public class Post
{
public int Id { get; set; }
public int BlogId { get; set; } // Required foreign key property
}
慣例不會探索此關聯性,因為沒有任何導覽指出這兩種類型相關。 它可以在 中 OnModelCreating
明確設定。 例如:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany<Post>()
.WithOne();
}
使用此組態時, Post.BlogId
屬性仍會依慣例偵測為外鍵,而且因為外鍵屬性不可為 Null,因此需要關聯性。 讓外鍵屬性可為 Null,即可將關聯性設為「選擇性」。
此關聯性更完整的明確組態為::
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany<Post>()
.WithOne()
.HasForeignKey(e => e.BlogId)
.IsRequired();
}
具有替代索引鍵的一對多
到目前為止所有範例中,相依的外鍵屬性受限於主體上的主鍵屬性。 外鍵可以改為限制為不同的屬性,然後成為主體實體類型的替代索引鍵。 例如:
// Principal (parent)
public class Blog
{
public int Id { get; set; }
public int AlternateId { get; set; } // Alternate key as target of the Post.BlogId foreign key
public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}
// Dependent (child)
public class Post
{
public int Id { get; set; }
public int BlogId { get; set; } // Required foreign key property
public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}
依慣例不會探索此關聯性,因為 EF 一律會依慣例建立與主鍵的關聯性。 您可以使用 對HasPrincipalKey
的呼叫明確設定OnModelCreating
。 例如:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany(e => e.Posts)
.WithOne(e => e.Blog)
.HasPrincipalKey(e => e.AlternateId);
}
HasPrincipalKey
可以與其他呼叫結合,以明確設定流覽、外鍵屬性和必要/選擇性本質。 例如:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany(e => e.Posts)
.WithOne(e => e.Blog)
.HasPrincipalKey(e => e.AlternateId)
.HasForeignKey(e => e.BlogId)
.IsRequired();
}
具有複合外鍵的一對多
到目前為止,在所有範例中,主體的主要或替代索引鍵屬性是由單一屬性所組成。 主要或替代索引鍵也可以從多個屬性形成--這些稱為 「復合索引鍵」。 當關聯性的主體具有複合索引鍵時,相依的外鍵也必須是具有相同屬性數目的複合索引鍵。 例如:
// Principal (parent)
public class Blog
{
public int Id1 { get; set; } // Composite key part 1
public int Id2 { get; set; } // Composite key part 2
public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}
// Dependent (child)
public class Post
{
public int Id { get; set; }
public int BlogId1 { get; set; } // Required foreign key property part 1
public int BlogId2 { get; set; } // Required foreign key property part 2
public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}
此關聯性是由慣例所探索。 不過,複合索引鍵本身必須明確設定::
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasKey(e => new { e.Id1, e.Id2 });
}
重要
如果其中一個屬性值為 Null,則複合外鍵值會被視為 null
。 具有一個屬性 Null 且另一個非 Null 的複合外鍵不會被視為與具有相同值的主要或替代索引鍵的相符專案。 這兩者都會被視為 null
。
HasForeignKey
和 HasPrincipalKey
都可以用來明確指定具有多個屬性的索引鍵。 例如:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
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();
});
}
提示
在上述程式代碼中,對 HasKey
和 HasMany
的呼叫已分組成巢狀產生器。 巢狀建置器不需要針對相同的實體類型呼叫 Entity<>()
多次,但功能上相當於呼叫 Entity<>()
多次。
不需要串聯刪除的必要一對多
// Principal (parent)
public class Blog
{
public int Id { get; set; }
public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}
// Dependent (child)
public class Post
{
public int Id { get; set; }
public int BlogId { get; set; } // Required foreign key property
public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}
根據慣例,必要的關聯性會設定為 串聯刪除;這表示刪除主體時,也會刪除其所有相依專案,因為沒有主體的相依性不能存在於資料庫中。 您可以設定 EF 擲回例外狀況,而不是自動刪除已不存在的相依資料欄:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany(e => e.Posts)
.WithOne(e => e.Blog)
.OnDelete(DeleteBehavior.Restrict);
}
自我參考一對多
在上述所有範例中,主體實體類型與相依實體類型不同。 情況不一定如此。 例如,在下列類型中,每個類型 Employee
都與其他 Employees
相關。
public class Employee
{
public int Id { get; set; }
public int? ManagerId { get; set; } // Optional foreign key property
public Employee? Manager { get; set; } // Optional reference navigation to principal
public ICollection<Employee> Reports { get; } = new List<Employee>(); // Collection navigation containing dependents
}
此關聯性是由 慣例所探索。 如果慣例不會探索關聯性的導覽、外鍵或必要/選擇性本質,則可以明確地設定這些專案。 例如:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Employee>()
.HasOne(e => e.Manager)
.WithMany(e => e.Reports)
.HasForeignKey(e => e.ManagerId)
.IsRequired(false);
}