Entity 間的關聯設計是 EF 應用的另一項重點。

  1. 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
  2. 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 不會自動建立關聯,需手動設定。
  3. Cascade Delete (串聯刪除)
    EF Core 關聯支援 Cascade Delete,標註 [Cascade] 會在刪除 Parent 時連同 Children 一起刪除, [ClientSetNull] 則是將已載入記憶體的 Dependent Entity 則 Foreign Key 設為 null, 至於未載入記憶體的 Dependent Entity 維持不變(需人工刪除或改指向有效的 Principal Entity)。 Fluent API 寫法為 .OnDelete(DeleteBehavior.Cascade)。
  4. 關聯設定方式
    [ForeignKey("自訂ForeignKey屬性名")] 可自訂 Foreign Key 屬性名稱; [InverseProperty] 用來自訂反向導覽屬性,主要適用於超過一組導覽屬性時。 如以下例子,Post 有 Author 及 Contributor 兩個導覽屬性關聯到 User, 當要由 User 反向關聯到 Post 時,可以選擇用何者作為 Foreign Key 形成關聯。
    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; }
    }    
    
    Fluent API 方法: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。
  5. 關聯種類
    一對多:如 Blog 與 Post 關係,前面講很多。
    一對一:一個 Blog 對一個 BlogImage。Fluent API 寫 HasOne().WithOne()
    多對多:Post 與 Tag 的關聯,Post 可有多個 Tag,每個 Tag 可找到多篇 Post。
  6. 一對多關聯查詢時如何帶入 Child Entity?
    有三種做法 Eager Loading(視為查詢內容的一部分一起載入)、Explicit Loading(稍後再明確下指令載入)、Lazy Loading(稍後引用到導覽屬性時自動載入)
  7. Eager Loading
    使用 .Include()。
    using (var context = new BloggingContext())
    {
        var blogs = context.Blogs
            .Include(blog => blog.Posts)
            .ToList();
    }
    
    可 Include() 多個導覽屬性,也可用 ThenInclude() 深入裡一層導覽屬性,例如:Blog 載入 Post、Post 載入作者 Author、依據作者載入 Photo。
     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() 會被無視,例如:
    using (var context = new BloggingContext())
    {
        var blogs = context.Blogs
            .Include(blog => blog.Posts)
            .Select(blog => new
            {
                Id = blog.BlogId,
                Url = blog.Url
            })
            .ToList();
    }
    
    若希望 Include() 無效時發出警告,可在 Startup.cs 設定 optionsBuilder.ConfigureWarnings(warnings => warnings.Throw(CoreEventId.IncludeIgnoredWarning));
  8. 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();
    }
    
  9. Lazy Loading (EF Core 2.1+)
    需安裝 Microsoft.EntityFrameworkCore.Proxies,並在 Startup.cs 引用:
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .UseLazyLoadingProxies()
            .UseSqlServer(myConnectionString);    
    
    接著將導覽屬性改為 virtual 即可:
    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; }
    }
    
    另一種做法是不使用 Proxy 而是在建構式接入 ILazyLoader 並改寫導覽屬性:
    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 需依賴 ILazyLoader,若要避免可改用 Action<object, string> 委派取代,參考
  10. 循環參考問題與序列化
    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 可調整設定避免這類問題:
    public void ConfigureServices(IServiceCollection services)
    {
        //...
    
        services.AddMvc()
            .AddJsonOptions(
                options => options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore
            );
        //...
    }
    
    另一個簡單解法是在會產生循環參考的屬性加上 [JsonIgnore]。

Notes of using EF Core to build relationships between entities.


Comments

Be the first to post a comment

Post a comment


76 - 11 =