EF Core 筆記 3 - Model 關聯設計
7 |
Entity 間的關聯設計是 EF 應用的另一項重點。
- Entity 關聯
先說一對多。以 Blog、Post 為例,關聯為一個 Blog 有很多篇 Post。
術語說明:public class Blog { public int BlogId { get; set; } //Primary Key public string Url { get; set; } public List<Post> Posts { get; set; } //參考導覽屬性 } public class Post { public int PostId { get; set; } //Primary Key public string Title { get; set; } public string Content { get; set; } public int BlogId { get; set; } //Foreign Key public Blog Blog { get; set; } //反向導覽屬性 }
- Post 是 Dependent Entity,有 Foreign Key 屬性,關聯中的 Child
- Blog 是 Principal Entity,有 Primary/Alternate Key 屬性,關聯中的 Parent
- Post.BlogId 是 Foreign Key
- Blog.BlogId 是 Principal Key
- Post.Blog 是參考導覽屬性(Reference Navigation Property)
- Blog.Posts 是集合導覽屬性(Collection Navigation Property)
- Post.Blog 是反向導覽屬性(Inverse Navigation Property),Child 指向 Parent
- EF Core 產生關聯方式
當 Entity 屬性型別無法直接映對資料庫型別(NVARCHAR、NUMBER、DATE... 等),該屬性會被 EF Core 視為導覽屬性,建立關聯。 若出現成對的導覽屬性,則它們會被設成同一關聯的反向導覽屬性。 (例如:Blog 裡出現 List<Post> Posts,Post 也有 Blog 指向 Parent) 另外,EF Core 也會依據屬性名稱建立 Foreign Key,例如:Post 有 BlogId、PostsBlogId、BlogBlogId 屬性時,會被視為 Foreign Key。 若有導覽屬性但未定義 Foreign Key,EF Core 會自動補上 Shadow 屬性(名稱為<導覽屬性名稱><Principal Key名稱>,如 BlogBlogId), 但建議明確定義清楚較佳。
注意:若兩個型別間出現多組導覽屬性,則 EF Core 不會自動建立關聯,需手動設定。 - Cascade Delete (串聯刪除)
EF Core 關聯支援 Cascade Delete,標註 [Cascade] 會在刪除 Parent 時連同 Children 一起刪除, [ClientSetNull] 則是將已載入記憶體的 Dependent Entity 則 Foreign Key 設為 null, 至於未載入記憶體的 Dependent Entity 維持不變(需人工刪除或改指向有效的 Principal Entity)。 Fluent API 寫法為 .OnDelete(DeleteBehavior.Cascade)。 - 關聯設定方式
[ForeignKey("自訂ForeignKey屬性名")] 可自訂 Foreign Key 屬性名稱; [InverseProperty] 用來自訂反向導覽屬性,主要適用於超過一組導覽屬性時。 如以下例子,Post 有 Author 及 Contributor 兩個導覽屬性關聯到 User, 當要由 User 反向關聯到 Post 時,可以選擇用何者作為 Foreign Key 形成關聯。
Fluent API 方法:public class Post { public int PostId { get; set; } public string Title { get; set; } public string Content { get; set; } public int AuthorUserId { get; set; } public User Author { get; set; } public int ContributorUserId { get; set; } public User Contributor { get; set; } } public class User { public string UserId { get; set; } public string FirstName { get; set; } public string LastName { get; set; } [InverseProperty("Author")] //關聯 Author 找出其為作者的所有文章 public List<Post> AuthoredPosts { get; set; } [InverseProperty("Contributor")] //關聯 Contributor 找出其為貢獻者的所有文章 public List<Post> ContributedToPosts { get; set; } }
modelBuilder.Entity<Post>().HasOne(p => p.BLog).WithMany(b => b.Posts);
建立 Blog 對 Post 一對多關聯。(可再串接 .HasForeignKey(p => p.BlogForeignKey) 宣告 Foreign Key,HasForeignKey() 支援多屬性組成複合式 Foreign Key)modelBuilder.Entity<Blog>().HasMany(b => b.Posts).WithOne();
建立單向關聯,Post 無導覽屬性指向 Blog。
另外,Foreign Key 不一定要是 Principal Entity 的 Primary Key,可使用 .HasPrincipalKey() 指定 Alternative Key (支援多屬性複合 Key)。
WithMany().IsRequired() 可定義必要性關聯,意指 Foreign Key 不可為 null,Child 一定要指向某個 Parent。 - 關聯種類
一對多:如 Blog 與 Post 關係,前面講很多。
一對一:一個 Blog 對一個 BlogImage。Fluent API 寫 HasOne().WithOne()
多對多:Post 與 Tag 的關聯,Post 可有多個 Tag,每個 Tag 可找到多篇 Post。 - 一對多關聯查詢時如何帶入 Child Entity?
有三種做法 Eager Loading(視為查詢內容的一部分一起載入)、Explicit Loading(稍後再明確下指令載入)、Lazy Loading(稍後引用到導覽屬性時自動載入) - Eager Loading
使用 .Include()。
可 Include() 多個導覽屬性,也可用 ThenInclude() 深入裡一層導覽屬性,例如:Blog 載入 Post、Post 載入作者 Author、依據作者載入 Photo。using (var context = new BloggingContext()) { var blogs = context.Blogs .Include(blog => blog.Posts) .ToList(); }
注意,若最後傳回結果不包含導覽屬性,則 Include() 會被無視,例如:using (var context = new BloggingContext()) { var blogs = context.Blogs .Include(blog => blog.Posts) .ThenInclude(post => post.Author) .ThenInclude(author => author.Photo) .Include(blog => blog.Owner) .ThenInclude(owner => owner.Photo) .ToList(); }
若希望 Include() 無效時發出警告,可在 Startup.cs 設定 optionsBuilder.ConfigureWarnings(warnings => warnings.Throw(CoreEventId.IncludeIgnoredWarning));using (var context = new BloggingContext()) { var blogs = context.Blogs .Include(blog => blog.Posts) .Select(blog => new { Id = blog.BlogId, Url = blog.Url }) .ToList(); }
- Excplicit Loading
針對集合物件取得 Collection()/Reference() 再呼叫 .Load(),範例:
另外,也可針對導覽屬性做查詢:using (var context = new BloggingContext()) { var blog = context.Blogs .Single(b => b.BlogId == 1); context.Entry(blog) .Collection(b => b.Posts) .Load(); context.Entry(blog) .Reference(b => b.Owner) .Load(); }
using (var context = new BloggingContext()) { var blog = context.Blogs .Single(b => b.BlogId == 1); var goodPosts = context.Entry(blog) .Collection(b => b.Posts) .Query() .Where(p => p.Rating > 3) .ToList(); }
- Lazy Loading (EF Core 2.1+)
需安裝 Microsoft.EntityFrameworkCore.Proxies,並在 Startup.cs 引用:
接著將導覽屬性改為 virtual 即可:protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder .UseLazyLoadingProxies() .UseSqlServer(myConnectionString);
另一種做法是不使用 Proxy 而是在建構式接入 ILazyLoader 並改寫導覽屬性:public class Blog { public int Id { get; set; } public string Name { get; set; } public virtual ICollection<Post> Posts { get; set; } } public class Post { public int Id { get; set; } public string Title { get; set; } public string Content { get; set; } public virtual Blog Blog { get; set; } }
以上做法 Blog 與 Post 需依賴 ILazyLoader,若要避免可改用 Action<object, string> 委派取代,參考public class Blog { private ICollection<Post> _posts; public Blog() { } private Blog(ILazyLoader lazyLoader) { LazyLoader = lazyLoader; } private ILazyLoader LazyLoader { get; set; } public int Id { get; set; } public string Name { get; set; } public ICollection<Post> Posts { get => LazyLoader.Load(this, ref _posts); set => _posts = value; } } public class Post { private Blog _blog; public Post() { } private Post(ILazyLoader lazyLoader) { LazyLoader = lazyLoader; } private ILazyLoader LazyLoader { get; set; } public int Id { get; set; } public string Title { get; set; } public string Content { get; set; } public Blog Blog { get => LazyLoader.Load(this, ref _blog); set => _blog = value; } }
- 循環參考問題與序列化
Blog 包含 Post 集合,Post 又有 Blog 屬性指向其所屬 Blog 的循環參考情境,會讓 Json.NET 等程式庫產生錯誤:Newtonsoft.Json.JsonSerializationException: Self referencing loop detected for property 'Blog' with type 'MyApplication.Models.Blog'.
ASP.NET Core 可調整設定避免這類問題:
另一個簡單解法是在會產生循環參考的屬性加上 [JsonIgnore]。public void ConfigureServices(IServiceCollection services) { //... services.AddMvc() .AddJsonOptions( options => options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore ); //... }
Notes of using EF Core to build relationships between entities.
Comments
# by Tommy
4. 關聯設定方式 發現EF Core 5.0 AuthorUserId及ContributorUserId必需可以為null,且必須使用[ForeignKey("Author")]、[ForeignKey("Contributor ")]指定為foreign key,update-database才可生效,不清楚是哪一代改動,又或是我哪裡做錯,請大大指教。 [ForeignKey("Author")] public int AuthorUserId { get; set; } [ForeignKey("Contributor ")] public int ContributorUserId { get; set; }
# by Tommy
上一則回復少了問號 4. 關聯設定方式 發現EF Core 5.0 AuthorUserId及ContributorUserId必需可以為null,且必須使用[ForeignKey("Author")]、[ForeignKey("Contributor ")]指定為foreign key,update-database才可生效,不清楚是哪一代改動,又或是我哪裡做錯,請大大指教。 [ForeignKey("Author")] public int? AuthorUserId { get; set; } [ForeignKey("Contributor ")] public int? ContributorUserId { get; set; }
# by Jeffrey
to Tommy, 我對 EF Core 的研究有限,推薦 EF 權威黃忠成老師 https://www.facebook.com/cooldotnet/
# by Tommy
了解,我再研究研究,謝謝回覆
# by Yu
您好,想請教一下, 如果關聯設定需要用到子entity的屬性作為key, 例如Blog需要Post裡面的Title去與第三個entity做mapping, 這在entity framework中是允許的嗎 我嘗試寫了像 HasPrincipleKey(blog => blog.post.Title) 但會有錯誤: The expression should represent a simple property access: 't => t.MyProperty'. Parameter name: propertyAccessExpression' 想請教您這種情況該怎麼在entity framework做
# by Jeffrey
to Yu, Key 會映對到關聯式資料庫的欄位(Column)組成的 Index,你無法在 Blog 資料表為另一個 Post 資料表的 Title 欄位建立索引,因此 EF 無法接受,得想其他方法實現。
# by Yu
了解,謝謝大大的回覆