前言
通過我發表的博文可知最近一段時間會將持續講解EntityFramework Core特性,在此之前我提到過Backing Fields,回頭翻了翻感覺寫的還不夠好,於是乎再來講解一番,也是自己再一次鞏固,廢話少說,開門見山。
EntityFramework Core Backing Fields基礎
Backing Fields特性出現於EF Core 1.1,我們姑且將其翻譯為返回字段,這樣翻譯和實際作用對應,Backing Fields允許EF Core讀或者寫到一個字段而非屬性,說的通俗易懂一點則是允許對字段進行映射。當屬性只有一個GET訪問器利用此特性將非常有用,在之前版本我們必須同時需要設置GET和SET訪問器,接下來我們詳細來講解Backing Fields(對字段進行映射)。
Backing Fields特性允許EF Core讀或者寫數據到字段中而不是屬性中。默認情況下滿足以下四種規則都會配置成Backing Fields。
- _<camel-cased property name>
- _<property name>
- m_<camel-cased property name>
- m_<property name>
我們首先給出本節需要用到的兩個類Blog和Post,如下:
public class Blog { public int Id { get; set; } public string Name { get; set; } public string Url { get; set; } public DateTime CreatedTime { get; set; } public DateTime ModifiedTime { get; set; } public ICollection<Post> Posts { get; set; } } public class Post { public int Id { get; set; } public string Name { get; set; } public int CommentCount { get; set; } public DateTime CreatedTime { get; set; } public DateTime ModifiedTime { get; set; } public Blog Blog { get; set; } }
根據如上對Backing Fields的約定來我們將Blog中的Url配置成Backing Fields,如下:
public class Blog { public int Id { get; set; } public string Name { get; set; } private string _url { get; set; } public string Url { get { return _url; } set { _url = value; } } public DateTime CreatedTime { get; set; } public DateTime ModifiedTime { get; set; } public ICollection<Post> Posts { get; set; } }
官網對於如上配置Backing Fields即(_url)如此解釋,在配置Backing Fields后,EF Core會將數據庫表中數據直接寫入該字段即(_url)。如果我們需要EF Core讀取或寫入值,那么將盡可能使用該屬性。 例如,如果需要EF Core更新某個屬性的值,那么它將使用屬性設置器(若已定義), 如果該屬性是只讀的,則將它寫入到字段中。想必如上解釋已經很明了,無需過多再闡述。我們來演示此種情況通過字段來設置屬性值,現在我們假設這樣一種情況,在創建我們自己博客時,此時我們的博客Url就需要確定下來,所以在添加Blog時我們將Ur以構造函數參數傳入給Backing Fields即_url,所以我們在上述基礎上添加構造函數如下:
public Blog(string url) { _url = url; }
接下來我們在控制台中創建Blog並添加到數據庫中,您可以先想象一下將會發生什么,如下:
using (var context = new EFCoreDbContext()) { context.Blogs.Add(new Blog("http://www.cnblogs/CreateMyself") { Name = "Jeffcky" }); context.SaveChanges(); foreach (var blog in context.Blogs) { Console.WriteLine($"{blog.Id} {blog.Name} {blog.Url}"); } }
因為EF Core默認是用無參構造函數實例化對象,既然我們自定義調用有參構造函數,所以必須顯式聲明無參構造函數。否則在遍歷數據時將拋出異常:System.InvalidOperationException:“A parameterless constructor was not found on entity type 'Blog'. In order to create an instance of 'Blog' EF requires that a parameterless constructor be declared.”。
除了上述我們根據給出的約定EF Core將其看作為返回字段外,我們仍然可以手動利用HasField進行配置,如下:
builder.Property(p => p.Url).HasField("_url");
除此之外我們還可通過UsePropertyAccessMode方法中的參數枚舉來配置對屬性的訪問模式,該參數枚舉存在如下三種:
比如我們需要對字段訪問模式為在構造函數中,那么我們可以進行如下配置:
builder.Property(p => p.Url).HasColumnType("VARCHAR(100)").UsePropertyAccessMode(PropertyAccessMode.FieldDuringConstruction);
這里需要注意的是所有訪問模式依然是通過GET或者SET訪問器,比如屬性設置為只讀即使進行了如上配置,依然是字段。上述參數枚舉說明詳情請見其具體定義而定。
上述我們是將屬性的字段進行映射,同時EF Core 1.1也支持不需要屬性而直接映射字段,比如我們在Blog中再定義如下字段:
public string _NonPropertyField;
接着我們進行如下映射配置,遷移后將在數據庫表中生成NonPropertyBackingField列並對應字段指向_NonPropertyField。
builder.Property<string>("NonPropertyBackingField").HasField("_NonPropertyField");
EntityFramework Core Backing Fields思考
到此是不是就這么簡單結束了呢?顯然不是,當學習任何一門技術時,所出現技術特性是為了解決問題而不是憑空產出,什么意思呢?當我們在自學過程中看官網例子時,官網將基礎知識一股腦全部灌輸給我們,那我們是不是應該不假思索下,它有什么用呢?比如上述Backing Fields特性的出現,因為我給您講解了,您就知道有這么個特性,但是不知道怎么用那和知道、了解有和區別呢?還不明白,接下來我們利用EF 6來看一個例子,通過此例子您就會頓悟了。請繼續往下看。
EntityFramework 6.x 沒有Backing Fields所帶來問題
我們創建EF 6.x控制台程序,給出如下測試類:
public class UseCase { public int Id { get; set; } private string _url { get; set; } public string Url { get { return _url; } } public string GetUrl() { return _url = "http://www.cnblogs.com/CreateMyself"; } }
接下來我們來添加數據看看,看看數據庫表是否能正常添加:
using (var ctx = new EfDbContext()) { var useCases = ctx.UseCases; var useCase = new UseCase(); useCase.GetUrl(); useCases.Add(useCase); ctx.SaveChanges(); };
在客戶端我們通過C#代碼設置了Url值,但是並未同步到數據庫表中,這也是EF 6.x中沒有解決的問題而在EF Core利用Backing Fields輕而易舉。我們能夠看到當訪問器GET或者SET中包含業務邏輯時這個時候就很能凸顯Backing Fields的實際作用。下面我們來看看在EF Core中的實際用途。
EntityFramework Core Backing Fields用途
我們知道在EntityFramework中導航屬性必須是ICollection<T>集合類型,如文章開頭我們定義Blog中的Posts導航屬性,我們也知道在ICollection<T>集合類型中存在Add、Remove、Clear等方法,這也就意味着有該集合類型的導航屬性我們都可以對其進行添加或刪除對象甚至於清除對象。正常情況下我們需要將實際業務行為代碼封裝在實體模型中,從這個角度出發,很顯然我們不能這么做。我們希望公開一個接口,通過該接口控制業務行為以及何時進行控制,以及何時應該發生怎樣的行為,這不僅僅是良好的領域驅動設計行為,也是很好的面向對象的設計行為。幸運的是在EntityFramework Core中對集合導航屬性不僅僅支持ICollection<T>,同時也支持IEnumerable<T>,此時我們將Blog中的Posts集合導航屬性修改成IEnemerable<Posts>,如下:
public IEnumerable<Post> Posts { get; set; }
這個時候我們定義集合類型為IEnumerable<T>,緊接着我們修改成如下形式。
public class Blog { public int Id { get; set; } public string Name { get; set; } public string Url { get; set; } public DateTime CreatedTime { get; set; } public DateTime ModifiedTime { get; set; } private readonly List<Post> _posts = new List<Post>(); public IEnumerable<Post> Posts => _posts.ToList(); }
那么問題就隨之而來,我們為何要修改成如上形式呢?如上定義私有的_posts返回字段並通過Posts來公開暴露,從安全角度看非常必須而且很有必要,當定義集合類型為ICollection<T>,此時我們能完全控制Post對象,也就是說能夠任意進行添加、刪除、清除操作。因為這完全屬於內部行為,無需對外暴露。當添加Post對象時,我們在Blog對象內部定義添加方法即可。
public void AddPost(Post post) { _posts.Add(post); }
那么問題又來了,我們定義了返回字段_posts后為何傳遞給對外暴露的Posts時要創建副本呢?因為對外暴露的Posts最終返回的是實際List<T>集合,所以最終還是會轉換成ICollection<T>集合類型,毫無疑問會造成性能的下降,所以我們需要通過創建副本來進行修正所以要ToList。還需要說明一點的是在EF Core 1.1版本中並不會映射上述私有的返回字段到數據存儲中,我們需要在OnModelCreating方法中進行如下配置:
var navigation = modelBuilder.Entity<Blog>().Metadata.FindNavigation(nameof(Blog.Posts)); navigation.SetPropertyAccessMode(PropertyAccessMode.Field);
如上代碼告訴EF Core通過命名約定發現它的字段並訪問Post屬性。直到EF Core 2.0仍然無法對導航屬性進行返回字段配置,只能對標量屬性進行返回字段配置。通過如下配置拋出異常可得知:
builder.Property(p => p.Posts).HasField("_posts");
通過github上提交的Issue得知對導航屬性進行返回字段的配置會在EF Core 2.1中實現,推薦為如下配置形式:
modelBuilder.Entity<Blog>() .HasMany( e => e.Posts, nb => nb.UsePropertyAccessMode(PropertyAccessMode.Field)) .WithOne( e => e.Blog, nb => nb.UsePropertyAccessMode(PropertyAccessMode.Field))
到此關於Backing Fields詳細說明就已結束,這里我們來一個完整性總結。使用Backing Fields的時機是:大部分情況下當屬性中訪問器存在業務邏輯時可能會用到Backing Fields,同時對於集合導航屬性 推薦使用如下組合方式。
- 定義私有只讀的返回字段(Backing Fields)。
- 定義公共的IEnumerable<T>接口屬性。
- 對返回字段創建副本傳遞給對外暴露的公共接口屬性
總結
侃侃而談如上諸多理論,在實際項目中或許直接定義集合導航屬性為ICollection<T>更加簡單粗暴,又或者趕項目進度誰會顧及那么多呢,能實現就行。精簡的內容,深入的講解,我們下節再會。