EntityFramework Core 1.1 Add、Attach、Update、Remove方法如何高效使用詳解
EntityFramework Core 1.1方法理論詳解
當我們利用EF Core查詢數據庫時如果我們不顯式關閉變更追蹤的話,此時實體是被追蹤的,關於變更追蹤我們下節再敘。就像我們之前在EF 6.x中討論的那樣,不建議手動關閉變更追蹤,對於有些特殊情況下,關閉變更追蹤可能會導致許多問題的發生。
實體狀態
對於EF Core 1.1中依然有四種狀態,有的人說不是有五種狀態麽,UnChanged、Added、Modified、Deleted、Detached。如果我們按照變更追蹤來劃分的話,實際上只有四種,將Detached排除在外,Detached不會被上下文所追蹤。那麽狀態如何改變的呢?內部有一個IStateManager接口,通過此接口來對實體狀態進行管理,此時再取決於SaveChanges被調用後背後是如何進行處理,我也就稍微看了下源碼,深入的東西沒去過多研究。
Added:實體還未插入到數據庫當中,當調用SaveChanges後將修改其狀態並將實體插入到數據庫。
UnChanged:實體存在數據庫中,但是在客戶端未進行修改,當調用SaveChanges後將忽略。
Modified:實體存在數據庫中,同時實體在客戶端也進行了修改,當調用SaveChanges後將更改其狀態並更新數據持久化到數據庫。
Deleted:實體存在數據庫中,當調用SaveChanges方法後將刪除實體。
實體方法
在EF Core 1.1中依然存在Add、Attach、Update方法,我們通過上下文或者DbSet<TEntity>能夠看到,當將實體傳遞到這些方法中時,它們與實體追蹤可達圖緊密聯系在一起,比如說我們之前討論的博客的導航屬性文章的發表,當我們添加文章的發表的這個實體時,然後調用Add方法後此時文章的發表這個實體也就被添加。在EF 6.x中我們說過當我們調用Add等方法時EF內部機制將會自動調用DetectChanges,但是在EF Core 1.1中則不再調用DetectChanges方法。空口無憑,我下載了源碼,如下:
public virtual void Add(TEntity item) { var entry = _stateManager.GetOrCreateEntry(item); if (entry.EntityState == EntityState.Deleted || entry.EntityState == EntityState.Detached) { OnCountPropertyChanging(); entry.SetEntityState(EntityState.Added); _count++; OnCollectionChanged(NotifyCollectionChangedAction.Add, item); OnCountPropertyChanged(); } }
上述我們沒有看到任何自動調用DetectChanges的邏輯,在EF 6.x中我們講到當調用SaveChanges時此時會回調DetectChanges,而在EF Core 1.1中同樣也是如此,所以相對於EF 6.x而言,EF Core 1.1只是在SaveChanges時回調DetectChanges,在Add、Attacth、Update等方法則不再回調DetectChanges,這樣的話性能就會好很多。我們看到源代碼中調用SaveChanges時邏輯如下:
public virtual int SaveChanges(bool acceptAllChangesOnSuccess) { CheckDisposed(); TryDetectChanges(); try { return StateManager.SaveChanges(acceptAllChangesOnSuccess); } catch (Exception exception) {..} }
接下來我們再來看看當調用Add、Update等方法時到底發生了什麽。
Add:當調用Add方法時就沒什麽可說的了,此時將在圖中的對應的所有實體推入到Added狀態,也就說在調用SaveChanges時將會插入到數據庫中去。
Attach:當調用Attach方法時將在圖中的所有實體推入到UnChanged狀態,但是有一個額外情況,比如我們在一個類中添加導航屬性數據時,此時Attach的話將會使用混合模式,將此實體的狀態為UnChanged而導航屬性的狀態則是Added狀態,所以當插入到數據庫中時,這個已存在的數據將不會被保存,只有新添加的導航屬性數據才會被插入到數據庫中去。
Update:Update方法和Attach方法一樣只是將其狀態修改為Modified,而將新添加的實體的修改將進行插入。
Remove:當調用Remove方法時此時它只會影響傳遞給該方法的實體,不會去遍歷實體的可到達圖。如果一個實體的狀態是UnChanged或者Modified,說明該實體已存在數據庫中,此時只需將其狀態修改為Deleted。如果實體的狀態為Added,此時說明該實體在數據庫中不存在,此時會脫離上下文而不被跟蹤。所以Remove方法側重強調實體要被追蹤,否則的話需要首先被Attach然後將其推入到Deleted狀態。
Range方法
在EF Core 1.1中多了AddRanges、UpdateRanges等方法,它們和實際調用多次調用非Range方法其實是一樣的,它內部也會去遍歷實體集合並更新其狀態,如下:
public virtual void UpdateRange([NotNull] IEnumerable<object> entities) => SetEntityStates(Check.NotNull(entities, nameof(entities)), EntityState.Modified);
我們再看SetEntityStates這個方法的實現。
private void SetEntityStates(IEnumerable<object> entities, EntityState entityState) { var stateManager = StateManager; foreach (var entity in entities) { SetEntityState(stateManager.GetOrCreateEntry(entity), entityState); } }
EF Core內部機制的處理肯定比我們之前手動去遍歷添加實體集合性能要高,意外看到一篇文章上有說僅僅只高效一點,因為Range方法自動調用DetectChanges方法,找了半天也沒看見在哪裏調用DetectChanges,郁悶,算是一點疑惑吧。
【註意】EF團隊之前一直在承諾EF Core會更高效和更高可擴展,但是我閱讀源碼發現內部還是自動調用了DetectChanges,性能方面的話還是不算太高效,但是,但是源碼中已經明確給出,關於DetectChanges方法,未來對於這個api會進行更改或者徹底移除,源碼註釋如下:
/// <summary> /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// </summary> void DetectChanges([NotNull] IStateManager stateManager);
追蹤圖
對於變更追蹤也好,默認啟用變更追蹤也好,我們都是通過ChangeTracker屬性來獲取到,如下:
EFCoreContext efCoreContext; efCoreContext.ChangeTracker.AutoDetectChangesEnabled; efCoreContext.ChangeTracker.DetectChanges;
在ChangeTracker中也有一個重要的方法那就是如下:
efCoreContext.ChangeTracker.TrackGraph;
我們暫且起名為跟蹤圖吧,它是對實體狀態的完全控制,比如我們在將數據插入到數據庫之前想設置其某一個值為臨時值,我們就可以通過該方法來實現。
Blog blog; using (var efCoreContext = new EFCoreContext(options)) { efCoreContext.ChangeTracker.TrackGraph(blog, node => { var entry = node.Entry; if ((int)entry.Property("Id").CurrentValue < 0) { entry.State = EntityState.Added; entry.Property("Id").IsTemporary = true; } else { entry.State = EntityState.Modified; } }); }
在EF Core 1.1其余的就是關於Add、Update等方法的異步操作了,對於操作數據庫不至於阻塞的情況也還是挺好的。
EntityFramework Core 1.1方法實踐詳解
關於EF Core 1.1中一些基本的知識我們過了一遍,下面我們來看看這些方法到底該如何高效使用呢?
Add/AddRange
關於這個方法就沒有太多敘述的了,對應的則是異步方法。我們重點看看其他的方法。
Update/UpdateRange
當我們根據主鍵去更新所有實體這個so easy了,我們在Blog表添加如下數據。
(1)更新方式一
現在我們查出Id=1的實體,然後將Name進行修改如下:
IBlogRepository _blogRepository; public HomeController(IBlogRepository blogRepository) { _blogRepository = blogRepository; } public IActionResult Index() { var blog = _blogRepository.GetSingle(d => d.Id == 1); blog.Name = "EntityFramework Core 1.1"; _blogRepository.Commit(); return View(); }
上述我們直接查詢出來主鍵對應的實體然後修改其值,最後提交更新其實體的對應修改的屬性。最後順理成章的數據字段進行了修改
我們知道因為查詢出來的實體在未關閉變更追蹤的情況下始終都是被追蹤的,所以必須進行對應修改,但是要是下面的情況呢。
public IActionResult Index(int Id,Blog blog) { return Ok(); }
在客戶端對數據進行了修改,我們需要根據主鍵Id進行對應屬性修改,當然不希望多此一舉的話,我們可以根據主鍵Id去查詢對應的實體,然後將屬性進行賦值最後提交修改保存到數據庫中,大概就演變成如下情況。
public IActionResult Index(int Id,Blog blog) { var oldBlog = _blogRepository.GetSingle(d => d.Id == Id); oldBlog.Name = blog.Name; oldBlog.Url = blog.Url; _blogRepository.Commit(); return Ok(); }
誠然上述方法能達到我的目的,其實還有簡便的方法,如下:
(2)更新方式二
既然有簡單的方法為何我們不用呢,這樣的場景就是更新指定屬性,以往的情況都是自己封裝一個Update方法,然後利用反射去包含需要修改的屬性接著更改其屬性的狀態為修改,最後提交修改即可。是的,這就是我們說的方法,但是,但是在EF Core 1.1中完全不需要我們去封裝,我們需要做的只是封裝成一個通用方法即可,內置實現EF Core已經幫我們實現,我們來看看。
void Update(T entity, params Expression<Func<T, object>>[] properties);
很熟悉吧,我們在基倉儲接口給出這樣一個接口,接著我們來實現此接口,如下:
public void Update(T entity, params Expression<Func<T, object>>[] properties) { _context.Entry(entity).State = EntityState.Unchanged; foreach (var property in properties) { var propertyName = ExpressionHelper.GetExpressionText(property); _context.Entry(entity).Property(propertyName).IsModified = true; } }
是不是夠簡單粗暴,開源就是好啊,查找資料時發現老外已經給出了具體實現,當直接調用時居然發現已經給我們封裝了,接下來我們再來修改指定的屬性就變成了如下:
public IActionResult Index() { var blog = new Blog() { Id = 1, Name = "EntityFramework Core 1.1" }; _blogRepository.Update(blog, d => d.Name); _blogRepository.Commit(); return Ok(); }
上述只是演示,實際項目當中時我們只需給出我們修改的主鍵和實體即可。如果是修改實體集合的話,再重載一個遍歷就ok。到這裏你是不是發現已經非常完美了,還有更完美的解決方案,請繼續往下看。
(3)更新方式三
其實在ASP.NET Core MVC中有比上面進一步還爽的方式通過利用TryUpdateModelAsync方法來實現,此方法有多個重載來實現,完全不需要我們去封裝。如下:
public async Task<IActionResult> Index() { var blog = _blogRepository.GetSingle(d => d.Id == 1); blog.Name = "EntityFramework Core 1.1"; await TryUpdateModelAsync(blog, "", d => d.Name); _blogRepository.Commit(); return Ok(); }
上述三種更新方式各有其應用場景,如果必須要總結的話就主要是第二種方式和第三種方式該如何取舍,第二種方式通過我們手動封裝的方式不需要再進行查詢,直接更改其狀態進行提交更新即可,而第三種方式需要進行查詢才會被追蹤最終提交更新,看個人覺得哪種方式更加合適就取哪種吧。關於EF Core 1.1中對於數據更新我們就講解完了,我們再來看看刪除。
Remove/RemoveRange
對於上述和更新一樣如果該實體已經被變更追蹤,直接調用內置的方法Delete方法即可,大部分場景下是根據主鍵去刪除數據。這裏有兩種方式供我們選擇,請往下看。
(1)刪除方式一
public IActionResult Index() { var blog = _blogRepository.GetSingle(d => d.Id == 1); _blogRepository.Delete(blog); _blogRepository.Commit(); return Ok(); }
我們查詢出需要刪除的實體,然後通過調用Remove(這裏我封裝了)方法將其標識為Deleted狀態進行刪除,當查詢數據我們可以關閉變更追蹤,一來數據量大的話對內存壓力不會太大,二來因為調用Remove方法會將其標識為Deleted狀態也會被追蹤,不會有任何問題。
(2)刪除方式二【推薦】
為了盡量減少請求時間,我們能一步完成的何必要用兩步呢,我們完全可以直接實例化一個實體,將其主鍵賦值,最後修改其狀態為Deleted,最終將持久化到數據庫中刪除對應的數據。如下:
public IActionResult Index() { var blog = new Blog() { Id = 1 }; _blogRepository.Delete(blog); _blogRepository.Commit(); return Ok(); }
Query
最後還剩下一個查詢沒有講述,這個和添加方法一樣,比較簡單我們稍微過一下即可。由於在EF Core中不再支持延遲加載,所以我們需要通過Include顯式獲取我們需要的導航屬性,比如如下:
DbContext dbContext; dbContext.Set<Blog>().Include(d => d.Posts);
如果有多個導航屬性,我們接著進行ThenInclude,如下:
DbContext dbContext; dbContext.Set<Blog>().AsNoTracking().Include(d => d.Posts).ThenInclude(d => d....).
為了避免這樣多次ThenInclude,方便調用我們進行如下封裝即可:
public T GetSingle(Expression<Func<T, bool>> predicate, params Expression<Func<T, object>>[] includeProperties) { IQueryable<T> query = _context.Set<T>(); foreach (var includeProperty in includeProperties) { query = query.Include(includeProperty); } return query.Where(predicate).FirstOrDefault(); }
此時我們只需要進行對應調用即可,大概如下:
_blogRepository.GetSingle( d=>d.Id == 1, p=>p.Posts, p=>....)
EntityFramework Core 1.1 Add、Attach、Update、Remove方法如何高效使用詳解