.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號: N170418201
出刊日期: 2017/4/5
Entity Framework提供異動追蹤應用程式開發介面(Change Tracking API)來存取記憶體實體的資料是否有異動,例如新增、刪除或修改實體的屬性。透過這些API,可以進一步得知實體屬性目前的值(Current Values)、原始值(Original Values),以及資料庫最新的值(Database Values)。此外,還可以掌握實體中哪一個屬性被修改了。本文將介紹如何透過Entity Framework中的Change Tracking API來存取實體的資料。
本文的範例將採用Entity Framework的Change Tracking Proxy來偵測異動,利用Code First From Database,從現有的Pubs資料庫中建立ADO.NET實體資料模型。讓我們從建立一個主控台專案開始,利用Visual Studio 2015來建立Console Application(主控台應用程式),從「File」-「New」-「Project」,在「New Project」對話盒中,確認視窗上方.NET Framework的目標版本為「.NET Framework 4.6」或以上,選取左方「Installed」-「Templates」-「Visual C#」程式語言,從分類中,選取「Console Application」,適當設定專案名稱與專案存放路徑,按下「OK」鍵,請參考下圖所示:
圖 1:建立一個主控台專案。
使用反向工程建立ADO.NET實體資料模型
下一步是使用反向工程的功能來定義模型,我們想要從SQL Server的Pubs範例資料庫來建立模型。從Visual Studio 2015開發工具-「Solution Explorer」- 專案名稱上方按滑鼠右鍵,從快捷選單選擇「Add」- 「New Item」選項,開啟「Add New Item」對話盒,從右上方文字方塊輸入「ado.net」搜尋,選取「ADO.NET Entity Data Model」,設定名稱為「PubsContext」,按下「Add」按鈕,請參考下圖所示:
圖 2:使用反向工程建立ADO.NET實體資料模型。
然後選取「Code First from database」,建立ADO.NET實體資料模型,請參考下圖所示:
圖 3:建立ADO.NET實體資料模型。
在下一個畫面,選取「New Connection」建立資料庫連接,請參考下圖所示:
圖 4:建立資料庫連接。
按「Next」按鈕進入到下一個畫面,以選取資料庫物件,請參考下圖所示:
圖 5:選取資料庫物件。
當精靈完成之後,專案中資料夾內將產生個多個C#檔案,模型架構請參考下圖所示:
圖 6:部分的實體資料模型
偵測實體的異動
DbContext的Entry()方法可以存取異動追蹤(Change Tracking)資訊,Entry方法回傳一個DbEntityEntry物件,透過此物件的屬性和方法可以操做實體的資料。
舉例來說,當你透過前文的步驟建立實體資料模型後,目前專案中會包含一個store類別,store類別包含了兩個宣告為virtual 的導覽屬性(Navigation Property),參考程式碼如下:
namespace PubsDemo
{
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.Spatial;
public partial class store
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
public store()
{
sales = new HashSet<sale>();
discounts = new HashSet<discount>();
}
[Key]
[StringLength(4)]
public string stor_id { get; set; }
[StringLength(40)]
public string stor_name { get; set; }
[StringLength(40)]
public string stor_address { get; set; }
[StringLength(20)]
public string city { get; set; }
[StringLength(2)]
public string state { get; set; }
[StringLength(5)]
public string zip { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<sale> sales { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<discount> discounts { get; set; }
}
}
在主控台專案Program類別的Main()方法中撰寫以下程式碼,使用DbEntityEntry<TEntity>泛型型別的State屬性來得到目前實體的狀態是「Added」、「Unchanged」、「Modified」,或是「Deleted」。參考以下範例程式碼,先取回Stores資料表中stor_id為「6380」的資料,然後叫用DbContext類別的Entry()方法取回DbEntityEntry<store>物件,接著利用DbEntityEntry<store>類別的State屬性印出目前實體的狀態。下一行程式碼又修改stor_name屬性的值,再印出目前實體的狀態:
using System;
using System.Collections.Generic;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PubsDemo
{
class Program
{
static void Main(string[] args)
{
using (var context = new PubsContext())
{
var aStore = context.stores.Where(s => s.stor_id == "6380").Single();
DbEntityEntry<store> entry = context.Entry(aStore);
Console.WriteLine(entry.State);
aStore.stor_name = "New Store Name";
Console.WriteLine(entry.State);
}
}
}
}
這個範例的執行結果會印出兩個Unchanged字串,請參考下圖所示:
圖 7:檢視狀態。
這是因為預設Code First From Database精靈會使用Snapshot Change Tracking模式來追蹤異動,我們需要手動叫用DbContext.ChangeTracker.DetectChanges方法來強制檢查是否有異動發生,修改程式碼如下:
using System;
using System.Collections.Generic;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PubsDemo
{
class Program
{
static void Main(string[] args)
{
using (var context = new PubsContext())
{
var aStore = context.stores.Where(s => s.stor_id == "6380").Single();
DbEntityEntry<store> entry = context.Entry(aStore);
Console.WriteLine(entry.State);
aStore.stor_name = "New Store Name";
context.ChangeTracker.DetectChanges();
Console.WriteLine(entry.State);
}
}
}
}
再次執行這個範例,則印出以下結果,從結果可得知修改完stor_name屬性之後,便偵測到資料已異動,而印出「Modified」狀態,請參考下圖所示:
圖 8:檢視狀態。
此外,根據文件的說明,當使用了以下的方法,就會觸發Entity Framework自動叫用DetectChanges()方法來偵測異動,因此大部分情況下,你不需要手動呼叫DetectChanges()方法:
- DbSet.Find
- DbSet.Local
- DbSet.Remove
- DbSet.Add
- DbSet.Attach
- DbContext.SaveChanges
- DbContext.GetValidationErrors
- DbContext.Entry
- DbChangeTracker.Entries
使用Change Tracking Proxy
Entity Framework提供Change Tracking Proxy,使用Change Tracking Proxy的好處是,任何對實體異動都能即時得知,否則需要手動叫用DetectChanges()方法來強制檢查是否有異動發生。DbContext提供的方法有許多都會自動呼叫DetectChanges()方法,而Entry方法是其中的一個,不過若只是要讀取Entity的屬性值,並不會自動叫用DetectChanges()方法。
若要使用Change Tracking Proxy模型類別必需滿足以下的必要條件:
- 類別必需標識為public,不可以是sealed類別。
- 模型中每一個屬性需要宣告為virtual虛擬屬性。
- 每一個屬性都必要要有標識為Public的getter與setter。
- 任何集合型別的導覽屬性(Navigation Property),其型別必需為ICollection<T>。
讓我們修改store模型以讓Entity Frameowk建立Change Tracking Proxy,刪除(或註解)建構函式中兩行齣始化導覽屬性的程式碼,並且在每一個屬性宣告加上virtual關鍵字:
namespace PubsDemo
{
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.Spatial;
public partial class store
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
public store()
{
//sales = new HashSet<sale>();
//discounts = new HashSet<discount>();
}
[Key]
[StringLength(4)]
public virtual string stor_id { get; set; }
[StringLength(40)]
public virtual string stor_name { get; set; }
[StringLength(40)]
public virtual string stor_address { get; set; }
[StringLength(20)]
public virtual string city { get; set; }
[StringLength(2)]
public virtual string state { get; set; }
[StringLength(5)]
public virtual string zip { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<sale> sales { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<discount> discounts { get; set; }
}
}
修改Main()方法:
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PubsDemo
{
class Program
{
static void Main(string[] args)
{
using (var context = new PubsContext())
{
var aStore = context.stores.Where(s => s.stor_id == "6380").Single();
DbEntityEntry<store> entry = context.Entry(aStore);
Console.WriteLine(entry.State);
aStore.stor_name = "New Store Name";
Console.WriteLine(entry.State);
}
}
}
}
這次執行,如預期結果一樣,一開始實體的狀態是Unchanged,在修改stor_name屬性之後,狀態就變為「Modified」,請參考下圖所示:
圖 9:使用Change Tracking Proxy。
即使強制AutoDetectChangesEnabled屬性設定為「false」關掉自動偵測異動功能,修改程式碼如下,執行的結果和上例一樣:
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PubsDemo
{
class Program
{
static void Main(string[] args)
{
using (var context = new PubsContext())
{
context.Configuration.AutoDetectChangesEnabled = false;
var aStore = context.stores.Where(s => s.stor_id == "6380").Single();
DbEntityEntry<store> entry = context.Entry(aStore);
Console.WriteLine(entry.State);
aStore.stor_name = "New Store Name";
Console.WriteLine(entry.State);
}
}
}
}
Current、Original與Database值
DbEntityEntry<T>類別提供了CurrentValues與OriginalValues兩個屬性可以取得目前屬性的值(CurrentValues)以及從資料庫查詢出來的屬性原始值(OriginalValues)。另外還有一個GetDatabaseValues()方法,可以取得目前資料庫資料的最新值。讓我們來試著利用程式碼,讀取Pubs資料庫Stores資料表資料,並觀察這些屬性的值,參考以下範例程式碼:
using System;
using System.Collections.Generic;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PubsDemo
{
class Program
{
static void Main(string[] args)
{
using (var context = new PubsContext())
{
var aStore = context.stores.Where(s => s.stor_id == "6380").Single();
DbEntityEntry<store> entry = context.Entry(aStore);
Console.WriteLine($"State : {entry.State}");
Console.WriteLine("OriginalValues:");
foreach (var propertyName in entry.OriginalValues.PropertyNames)
{
Console.WriteLine($"\t{propertyName} : {entry.OriginalValues[propertyName]}");
}
Console.WriteLine("====================================================");
aStore.stor_name = "New Eric the Read Books 1";
Console.WriteLine($"State : {entry.State}");
Console.WriteLine("CurrentValues:");
foreach (var propertyName in entry.CurrentValues.PropertyNames)
{
Console.WriteLine($"\t{propertyName} : {entry.CurrentValues[propertyName]}");
}
Console.WriteLine("====================================================");
context.Database.ExecuteSqlCommand("Update stores SET stor_name='New Eric the Read Books 2' WHERE stor_id='6380'");
Console.WriteLine($"State : {entry.State}");
Console.WriteLine("DatabaseValues:");
foreach (var propertyName in entry.GetDatabaseValues().PropertyNames)
{
Console.WriteLine($"\t{propertyName} : {entry.GetDatabaseValues()[propertyName]}");
}
}
}
}
}
範例程式先利用Where()方法,從資料庫篩選出stor_id為「6380」的資料,接著叫用Single()方法取得代表此記錄的Store實體物件。跟著利用DbContext類別的Entry()方法,取得DbEntityEntry<store>物件。從DbEntityEntry<store>的State屬性我們可以知道第一次從資料庫將此筆資料取回時,store實體的狀態是「Unchanged」,第一個foreach迴圈將store實體所有屬性的OriginalValues印出,此時印出的值和資料庫目前的值是一致的。
下一段程式碼則將stor_name屬性修改為「New Eric the Read Books 1」,因此,如預期一般,從DbEntityEntry<store>的State屬性我們可以知道store實體的狀態是「Modified」,然後利用foreach迴圈將所有屬性的CurrentValues印出,目前stor_name屬性CurrentValues為「New Eric the Read Books 1」。
最後一段程式碼利用Database.ExecuteSqlCommand直接執行SQL語法,將Stores資料表6380這筆資料的Stor_name欄位值修改為「New Eric the Read Books 2」,然後再利用DbEntityEntry<store>類別的GetDatabaseValues()方法取得資料庫最新的值,此範例執行結果參考如下:
State : Unchanged
OriginalValues:
stor_id : 6380
stor_name : Eric the Read Books
stor_address : 788 Catamaugus Ave.
city : Seattle
state : WA
zip : 98056
====================================================
State : Modified
CurrentValues:
stor_id : 6380
stor_name : New Eric the Read Books 1
stor_address : 788 Catamaugus Ave.
city : Seattle
state : WA
zip : 98056
====================================================
State : Modified
DatabaseValues:
stor_id : 6380
stor_name : New Eric the Read Books 2
stor_address : 788 Catamaugus Ave.
city : Seattle
state : WA
zip : 98056
此範例執行結果請參考下圖所示:
圖 10:檢測狀態。
使用Reload()方法重新載入實體資料
有時使用者從資料庫載入實體資料(Entry)進行修改,後續又想要取消修改,那麼最簡單的方式便是從資料庫重新載入資料。DbEntityEntry包含一個Reload()方法可以從資料庫重新載入最新資料。
參考以下範例程式碼先載入資料庫stores資料表stor_id欄位為「6380」的記錄到store模型,叫用Entry()方法取得DbEntityEntry<store>物件,印出stor_name屬性目前的值,然後修改stor_name欄位的值為「New Eric the Read Books 1」,印出stor_name屬性目前的值。接著再叫用Reload()方法重新載入實體資料,再印出stor_name屬性目前的值:
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PubsDemo
{
class Program
{
static void Main(string[] args)
{
using (var context = new PubsContext())
{
var aStore = context.stores.Where(s => s.stor_id == "6380").Single();
DbEntityEntry<store> entry = context.Entry(aStore);
string curValue = entry.CurrentValues["stor_name"].ToString();
Console.WriteLine($" CurrentValues : {curValue}");
aStore.stor_name = "New Eric the Read Books 1";
curValue = entry.CurrentValues["stor_name"].ToString();
Console.WriteLine($" CurrentValues : {curValue}");
context.Entry(aStore).Reload();
curValue = entry.CurrentValues["stor_name"].ToString();
Console.WriteLine($" CurrentValues : {curValue}");
}
}
}
}
此範例執行結果如下,修改stor_name屬性之前,stor_nam屬性的值為「Eric the Read Books」,修改完之後,stor_nam屬性的值為「New Eric the Read Books 1」,叫用Reload()方法重新載入資料庫資料後,stor_nam屬性的值為「Eric the Read Books」:
CurrentValues : Eric the Read Books
CurrentValues : New Eric the Read Books 1
CurrentValues : Eric the Read Books
新增資料
新增實體資料時,只會保有Current值,沒有Original與Database值,若資料未新增到資料庫之前,試著讀取這兩個值將會得到例外錯誤。參考以下範例程式碼,範例執行時只能夠讀取CurrentValues,讀取OriginalValues與叫用GetDatabaseValues()方法時,會發生例外錯誤:
using System;
using System.Collections.Generic;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PubsDemo
{
class Program
{
static void Main(string[] args)
{
using (var context = new PubsContext())
{
var aStore = new store() { stor_id = "9999", stor_name = "9999 store" };
context.stores.Add(aStore);
DbEntityEntry<store> entry = context.Entry(aStore);
Console.WriteLine($"State : {entry.State}");
//context.SaveChanges();
Console.WriteLine("OriginalValues:");
foreach (var propertyName in entry.OriginalValues.PropertyNames)
{
Console.WriteLine($"\t{propertyName} : {entry.OriginalValues[propertyName]}");
}
Console.WriteLine("====================================================");
Console.WriteLine($"State : {entry.State}");
Console.WriteLine("CurrentValues:");
foreach (var propertyName in entry.CurrentValues.PropertyNames)
{
Console.WriteLine($"\t{propertyName} : {entry.CurrentValues[propertyName]}");
}
Console.WriteLine("====================================================");
Console.WriteLine($"State : {entry.State}");
Console.WriteLine("DatabaseValues:");
foreach (var propertyName in entry.GetDatabaseValues().PropertyNames)
{
Console.WriteLine($"\t{propertyName} : {entry.GetDatabaseValues()[propertyName]}");
}
}
}
}
}
若將「context.SaveChanges();」這行程式碼的註解移除後執行,則會得到以下的執行結果,一開始建立一個新的store物件代表要新增的資料,並將之加入context.stores集合中,此時印出它的狀態為「Added」。
只要叫用SaveChanges()方法,則其狀態便會變成「Unchanged」,也可以得到Unchanged物件的CurrentValues與DatabaseValues,請參考下圖所示:
圖 11:檢視新增資料狀態。
新增資料-Change Tracking Proxy
特別要注意的是,使用new關鍵字來建立實體物件,並不會建立Change Tracking Proxy。 若想要使用Change Tracking Proxy,你可以改用DbSet類別提供的Create()方法來建立要新增的實體物件,參考以下程式碼範例,建立實體物件後,再利用add()方法加入DbContext:
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PubsDemo
{
class Program
{
static void Main(string[] args)
{
using (var context = new PubsContext())
{
var aStore = context.stores.Create<store>();
aStore.stor_id = "9999";
aStore.stor_name = "9999 store";
context.stores.Add(aStore);
DbEntityEntry<store> entry = context.Entry(aStore);
Console.WriteLine($"State : {entry.State}");
Console.WriteLine("CurrentValues:");
foreach (var propertyName in entry.CurrentValues.PropertyNames)
{
Console.WriteLine($"\t{propertyName} : {entry.CurrentValues[propertyName]}");
}
context.SaveChanges();
}
}
}
}
範例程式執行時,使用除錯模式進行觀察,aStore的型別參考如下圖所示:
圖 12:Dynamic Proxy。
刪除資料
Entity Framework不記錄要刪除資料的CurrentValues,若試著讀取CurrentValues時,則會產生例外錯誤。參考以下範例程式碼建立一個store物件,stor_id屬性值為「9999」,代表要刪除的資料。
using System;
using System.Collections.Generic;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PubsDemo
{
class Program
{
static void Main(string[] args)
{
using (var context = new PubsContext())
{
var aStore = new store() { stor_id = "9999" };
DbEntityEntry<store> entry = context.Entry(aStore);
Console.WriteLine($"State : {entry.State}");
context.stores.Attach(aStore);
Console.WriteLine($"State : {entry.State}");
context.stores.Remove(aStore);
Console.WriteLine($"State : {entry.State}");
Console.WriteLine("OriginalValues:");
foreach (var propertyName in entry.OriginalValues.PropertyNames)
{
Console.WriteLine($"\t{propertyName} : {entry.OriginalValues[propertyName]}");
}
Console.WriteLine("====================================================");
Console.WriteLine($"State : {entry.State}");
Console.WriteLine("CurrentValues:");
foreach (var propertyName in entry.CurrentValues.PropertyNames) //Exception
{
Console.WriteLine($"\t{propertyName} : {entry.CurrentValues[propertyName]}");
}
}
}
}
}
一開始建立store物件aStore的狀態是「Detached」;叫用Attach()方法加入DbSet<store>後狀態變更為「Unchanged」;叫用Remove方法,則狀態會變更為「Deleted」。因為Deleted物件無CurrentValues,所以上述範例在執行到最後一段foreach方法時,會產生例外錯誤。
刪除資料-Change Tracking Proxy
同樣地,若需要使用到Change Tracking Proxy,那麼必需利用DbSet的Create方法來建立物件,參考以下範例程式碼所示:
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PubsDemo
{
class Program
{
static void Main(string[] args)
{
using (var context = new PubsContext())
{
var aStore = context.stores.Create<store>();
aStore.stor_id = "9999";
DbEntityEntry<store> entry = context.Entry(aStore);
Console.WriteLine($"State : {entry.State}");
context.stores.Attach(aStore);
Console.WriteLine($"State : {entry.State}");
context.stores.Remove(aStore);
Console.WriteLine($"State : {entry.State}");
context.SaveChanges();
}
}
}
}