EF實體類的配置可以使用數據注釋或Fluent API兩種方式配置,Fluent API配置的關鍵在於搞清實體類的依賴關系,按此方法配置,快速高效合理。為了方便理解,我們使用簡化的實體A和B以及A、B的配置類AMap和BMap,來演示如何正確配置實體類關系的過程。

public class A { public int Id { get; set; } } public class B { public int Id { get; set; } } public class AMap : EntityTypeConfiguration<A> { public AMap() { this.HasKey(o => o.Id); } } public class BMap : EntityTypeConfiguration<B> { public BMap() { this.HasKey(o => o.Id); } }
一、確定依賴關系:
假設實體B依賴於實體A(B->A),那么實體B中存在對實體A的引用。
二、實體類配置應該寫在哪里?
假設B依賴於A(B->A),很顯然,我們希望的是B表中生成外鍵(A表的主鍵值)。以下兩種方式都可以實現相同的表結構,但毫無疑問我們應該在B的配置文件BMap中進行關系配置。
(1)B依賴於A,A可以對B的存在一無所知。
(2)A可以單獨存在,配置寫在哪里都不會對A表產生影響。
(3)B對A的依賴是通過在B表中生成外鍵(A表的主鍵)。
推薦的寫法:
public class BMap : EntityTypeConfiguration<B> { public BMap() { this.HasRequired(o => o.A).WithMany(o=>o.ListB); } }
摒棄的寫法:
public class AMap : EntityTypeConfiguration<A> { public AMap() { this.HasMany(o => o.ListB).HasRequired(o => o.A); } }
依賴的方向決定了使用的配置,這在實體類數量和關系復雜時尤其重要,假設有10個實體類依賴A,混合書寫配置顯然不可取,而在被依賴實體中配置的結果會導致經常修改A的配置文件,你甚至不肯定修改了類A的配置文件是會引起A表的變化。
三、配置依賴關系
配置文件的基類EntityTypeConfiguration包含了一系列Has方法用來配置實體類,其中HasOptional和HasRequired根據實體的引用屬性配置實體關系。假設B依賴於A(B->A),HasOptional允許B單獨存在,這將在B表中生成可空的外鍵。HasRequired不允許B單獨存在,這將在B表中生成非空的外鍵。

public class BMap : EntityTypeConfiguration<B> { public BMap() { this.HasKey(o => o.Id); this.HasRequired(o => o.A); } }

public class BMap : EntityTypeConfiguration<B> { public BMap() { this.HasKey(o => o.Id); this.HasOptional(o => o.A); } }
四、配置關聯類型
HasOptional和HasRequired分別返回OptionalNavigationPropertyConfiguration和RequiredNavigationPropertyConfiguration對象,我們使用其中的WithMany和WithOptional來配置關聯的類型。
如果A:B = 1:N,我們使用WithMany。

public class BMap : EntityTypeConfiguration<B> { public BMap() { this.HasKey(o => o.Id); this.HasOptional(o => o.A).WithMany(); } }

public class BMap : EntityTypeConfiguration<B> { public BMap() { this.HasKey(o => o.Id); this.HasRequired(o => o.A).WithMany(); } }
如果A:B= 1:1,我們使用WithOptional。1:1的關聯要求外鍵的非空和唯一,數據庫是通過表B的外鍵作為主鍵來實現。

public class BMap : EntityTypeConfiguration<B> { public BMap() { this.HasKey(o => o.Id); this.HasRequired(o => o.A).WithOptional(); } }
四、可選導航屬性
導航屬性由關聯類型決定,但其存在與否不會影響實體的依賴關系和關聯類型。
對於B->A,如果A:B = 1:N,我們可以在A中添加ICollection<B>類型的導航屬性,同時修改關系配置,將該屬性傳遞給WithMany方法。

public class A { public int Id { get; set; } public ICollection<B> BList { get; set; } } public class B { public int Id { get; set; } public A A { get; set; } } public class AMap : EntityTypeConfiguration<A> { public AMap() { this.HasKey(o => o.Id); } } public class BMap : EntityTypeConfiguration<B> { public BMap() { this.HasKey(o => o.Id); this.HasOptional(o => o.A).WithMany(o => o.BList); } }
如果A:B = 1:1,我們可以在A中添加B類型的導航屬性,同時修改關系配置,將該屬性傳遞給WithOptional方法。

public class A { public int Id { get; set; } public B B { get; set; } } public class B { public int Id { get; set; } public A A { get; set; } } public class AMap : EntityTypeConfiguration<A> { public AMap() { this.HasKey(o => o.Id); } } public class BMap : EntityTypeConfiguration<B> { public BMap() { this.HasKey(o => o.Id); this.HasRequired(o => o.A).WithOptional(o => o.B); } }
五、顯式外鍵屬性
對於B->A,如果A:B = 1:1,外鍵就是主鍵。
如果A:B = 1:N,我們可以自定義導航屬性對應的外鍵屬性,首先在B中添加顯式的用於外鍵的屬性。

public class B { public int Id { get; set; } public A A { get; set; } //public int AId { get; set; } public int? AId { get; set; } }
WithMany返回DependentNavigationPropertyConfiguration對象,我們使用該對象的HasForeignKey方法,如果實體聯系配置為HasOptional,則需要使用可空類型匹配。

public class BMap : EntityTypeConfiguration<B> { public BMap() { this.HasKey(o => o.Id); this.HasOptional(o => o.A).WithMany().HasForeignKey(o => o.AId); } }
六、級聯刪除配置
HasForeignKey返回CascadableNavigationPropertyConfiguration對象,EF默認開啟級聯刪除,當實體關系復雜導致無法開啟級聯刪除時,我們使用該對象的WillCascadeOnDelete方法配置取消級聯刪除。
七、關於雙向依賴
EF中實體的關聯通過表的外鍵實現,1:N還是1:1都是通過外鍵實現。我們可以根據1:N配置的方式配置出雙向依賴的表,但通常所謂的多對多都不是雙向依賴。例如用戶和角色、學生和課程、文章和標簽等,甚至根本沒有依賴,因為二者都可以獨立存在,有的只是映射關系對二者的依賴,而這是1:N的問題。
我們使用EntityTypeConfiguration配置實體依賴,該類的ToTable、HasKey等實例方法都用於配置當前實體類映射的Table。HasRequired和HasOptional方法也會在對應的Table中生存外鍵,而HasMany方法則是其中的異類,偏偏配置的非當前實體類。
HasMany、WithMany除了在配置雙向引用時替我們自動生成關系表,帶來更多的是配置混亂。而所謂的自動生成關系表更是打破了我們實體類和Table的一一對應。在Microsoft.AspNet.Identity.EntityFramework 1.0中,我們可以看到IdentityUser和IdentityRole並沒有通過雙向引用自動生成關系表,而是定義了IdentityUserRole實體類用來映射:通過IdentityDbContext<TUser>的OnModelCreating配置我們可以看到雖然使用了HasMany配置TUser的Roles屬性,但是完全可以在IdentityUserRole中配置。即使在2.0版本中依舊如此。
八、常見的配置舉例
1.用戶和角色:
(1)確定依賴關系:User和Role都可以單獨存在,但UserRole不可以單獨存在,因此存在的依賴是UserRole->User,UserRole->Role。
(2)配置依賴關系:UserRole不能單獨存在,因此使用HasRequired。
(3)確定關聯類型:User:UserRole==1:*;Role:UserRole=1:*,因此使用WithMany。
(4)顯式的外鍵屬性:在UserRole中添加UserId和RoleId作為顯式的外鍵屬性。
(5)可選的導航屬性:在User和Role中添加ICollection<UserRole>類型的導航屬性。
UserRole不應該存在重復的用戶角色映射,因此使用外鍵作為聯合主鍵。

public class User { public User() { this.UserRoles = new List<UserRole>(); } public int Id { get; set; } public string UserName { get; set; } public ICollection<UserRole> UserRoles { get; set; } } public class Role { public Role() { this.UserRoles = new List<UserRole>(); } public int Id { get; set; } public string RoleName { get; set; } public ICollection<UserRole> UserRoles { get; set; } } public class UserRole { public User User { get; set; } public int UserId { get; set; } public Role Role { get; set; } public int RoleId { get; set; } } public class UserRoleMap : EntityTypeConfiguration<UserRole> { public UserRoleMap() { this.HasKey(o => new { o.UserId, o.RoleId }); this.HasRequired(o => o.User).WithMany(o => o.UserRoles).HasForeignKey(o => o.RoleId); this.HasRequired(o => o.Role).WithMany(o => o.UserRoles).HasForeignKey(o => o.UserId); } }
2.節點樹:
(1)確定依賴關系:Category自依賴,Category->Category
(2)配置依賴關系:Category可以單獨存在,因此使用HasOptional。
(3)確定關聯類型:Category:Category==1:*,因此使用WithMany。
(4)顯式的外鍵屬性:在UserRole中添加ParentId,由於Category可以單獨存在,ParentId為可空類型。
(5)可選的導航屬性:在Category中添加ICollection<Category>類型的導航屬性。

public class Category { public int Id { get; set; } public string Name { get; set; } public int? ParentId { get; set; } public Category Parent { get; set; } public ICollection<Category> Children { get; set; } } public class CategoryMap : EntityTypeConfiguration<Category> { public CategoryMap() { this.HasKey(o => o.Id); this.HasOptional(o => o.Parent).WithMany(o => o.Children).HasForeignKey(o => o.ParentId); } }