.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號: N170418201
出刊日期: 2017/4/19
本文延續《Change Tracking API - 1》一文的情境,介紹Entity Framework提供的異動追蹤應用程式開發介面(Change Tracking API)來讀取記憶體實體的資料是否有異動,並進一步利用這些API來修改實體或相關聯的資料。
使用GetValue<TValue>讀屬性值
前述《Change Tracking API - 1》一文的範例是利用索引子(Indexer)語法,利用 [ ] 中括號,括號中使用屬性名稱來讀取OriginalValues、CurrentValues以及資料庫的最新值,因使用索引子語法讀取出來的值其型別為object,所以你需要手動利用轉型的語法,將它轉換成適當型別,以利後續處理。除了利用索引子語法之外,還可以利用DbPropertyValues類別的GetValue<TValue>()方法來讀這些值,其中的TValue用來指定值的型別,這樣就不需要手動轉型。
參考以下範例程式碼,先載入資料庫stores資料表stor_id為「6380」的記錄到store實體,叫用Entry()方法取得DbEntityEntry<store>物件,然後設定stor_name的值為「New Eric the Read Books 1」,接著印出stor_name的CurrentValues、OriginalValues與DatabaseValue,因為stor_name屬性的型別為string,因此我們可以利用GetValue<string>("stor_name")語法來取得屬性值,並直接指定給string型別的變數:
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);
aStore.stor_name = "New Eric the Read Books 1";
Console.WriteLine($"State : {entry.State}");
string oriValue = entry
.OriginalValues
.GetValue<string>("stor_name");
Console.WriteLine($" OriginalValues : {oriValue}");
string curValue = entry
.CurrentValues
.GetValue<string>("stor_name");
Console.WriteLine($" CurrentValues : {curValue}");
string dbValue = entry
.GetDatabaseValues()
.GetValue<string>("stor_name");
Console.WriteLine($" DatabaseValues : {dbValue}");
}
}
}
}
此範例的執行結果將印出以下值:
State : Modified
OriginalValues : Eric the Read Books
CurrentValues : New Eric the Read Books 1
DatabaseValues : Eric the Read Books
將DbPropertyValues值轉換成物件
為了方便透過DbPropertyValues來操作資料,DbPropertyValues類別提供一個ToObject()方法,可以將CurrentValues、OriginalValues與DatabaseValues直接轉換成一個物件,參考以下範例程式碼:
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);
aStore.stor_name = "New Eric the Read Books 1";
store curValue = (store)entry
.CurrentValues.ToObject();
Console.WriteLine($" CurrentValues : {curValue.stor_name}");
store oriValue = (store)entry
.OriginalValues.ToObject();
Console.WriteLine($" OriginalValues : {oriValue.stor_name}");
store dbValue = (store)context
.Entry(aStore)
.GetDatabaseValues()
.ToObject();
Console.WriteLine($" DatabaseValues : {dbValue.stor_name}");
}
}
}
}
ToObject()方法只會將純量屬性(Scalar Property)的值複製到新物件,不複製導覽屬性(Navigation Property)的值。此範例的執行結果將印出以下值:
CurrentValues : New Eric the Read Books 1
OriginalValues : Eric the Read Books
DatabaseValues : Eric the Read Books
使用DbPropertyValues修改屬性值
DbPropertyValues修改屬性值也可以用來修改屬性值,參考以下範例程式碼先載入資料庫stores資料表stor_id為「6380」的記錄,叫用Entry()方法取得DbEntityEntry<store>物件,然後利用索引子,設定stor_name的值為「Eric the Read Books」,接著印出stor_name的CurrentValues、OriginalValues與DatabaseValues:
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);
entry.CurrentValues["stor_name"] = "Eric the Read Books";
store curValue = (store)entry
.CurrentValues.ToObject();
Console.WriteLine($" CurrentValues : {curValue.stor_name}");
store oriValue = (store)entry
.OriginalValues.ToObject();
Console.WriteLine($" OriginalValues : {oriValue.stor_name}");
store dbValue = (store)context
.Entry(aStore)
.GetDatabaseValues()
.ToObject();
Console.WriteLine($" DatabaseValues : {dbValue.stor_name}");
Console.WriteLine(aStore.stor_name);
context.SaveChanges();
}
}
}
}
此範例的執行結果將印出以下值,從結果可以看出,使用 CurrentValues修改stor_name,會連動修改stor_name屬性的值:
CurrentValues : Eric the Read Books
OriginalValues : New Eric the Read Books 1
DatabaseValues : New Eric the Read Books 1
Eric the Read Books
使用Change Tracking API的好處是,不需要叫用DetectChanges()方法,就可以偵測到屬性值的異動。舉例來說,修改上述程式碼,將AutoDetectChangesEnabled屬性設定為「false」關掉自動偵測異動功能,執行的結果和上例一樣,修改CurrentValues會連動修改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())
{
context.Configuration.AutoDetectChangesEnabled = false;
var aStore = context.stores.Where(s => s.stor_id == "6380").Single();
DbEntityEntry<store> entry = context.Entry(aStore);
entry.CurrentValues["stor_name"] = "New Eric the Read Books 1";
store curValue = (store)entry
.CurrentValues.ToObject();
Console.WriteLine($" CurrentValues : {curValue.stor_name}");
store oriValue = (store)entry
.OriginalValues.ToObject();
Console.WriteLine($" OriginalValues : {oriValue.stor_name}");
store dbValue = (store)context
.Entry(aStore)
.GetDatabaseValues()
.ToObject();
Console.WriteLine($" DatabaseValues : {dbValue.stor_name}");
Console.WriteLine(aStore.stor_name);
//context.SaveChanges();
}
}
}
}
此範例執行結果如下所示:
CurrentValues : New Eric the Read Books 1
OriginalValues : Eric the Read Books
DatabaseValues : Eric the Read Books
New Eric the Read Books 1
使用SetValues()方法複製DbPropertyValues值
SetValues()方法可以複製DbPropertyValues的值。假設你想要提供一個功能,讓使用者選擇是否放棄目前修改的資料,那麼最簡單的作法,就是讓CurrentValues回復到OriginalValues原始值,然後將目前DbEntityEntry的狀態改為「Unchanged」。
參考以下範例程式碼,先取回Stores資料表中stor_id為「6380」的資料,然後叫用DbContext類別的Entry方法取回DbEntityEntry<store>物件,下一行程式碼修改stor_name屬性的值為「"New Eric the Read Books 1"」,接著根據使用者輸入的資料,若選擇取消修改(y),則叫用SetValues方法,將原始值複製到CurrentValues,否則則叫用SaveChanges()方法,將異動寫到資料庫:
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(aStore.stor_name);
aStore.stor_name = "New Eric the Read Books 1";
Console.WriteLine("Cancel Update? (y/n)");
var r = Console.ReadLine();
if (r.ToLower() == "y")
{
entry.CurrentValues.SetValues(entry.OriginalValues);
entry.State = EntityState.Unchanged;
}
else {
context.SaveChanges();
}
Console.WriteLine(aStore.stor_name);
}
}
}
}
SetValues()方法可以傳入DbPropertyValues物件,或是任意物件,根據你傳入SetValues()方法的物件屬性名稱做比對,複製名稱相符的屬性值。若傳入SetValues()方法的物件屬性型別和DbPropertyValues不相符,則產生例外錯誤。若傳入SetValues()方法的物件屬性不存在於DbPropertyValues物件,則此屬性將會被忽略。
此範例執行結果如下圖所示:
圖 1:取消更新。
Property()方法
Property()方法可以操做一個純量(Scalar)或複雜(Complex)屬性。我們可以利用Property()方法來讀寫屬性原始值(Original Value)與目前值(Current Value),也可以用來識別屬性的狀態是否為「Modified」。
參考以下範例程式碼先載入資料庫stores資料表stor_id為「6380」的記錄,叫用Entry()方法取得DbEntityEntry<store>物件,然後利用強型別的Property()方法,傳入Lambda運算式,設定stor_name的值為「New Eric the Read Books 1」,接著印出stor_name的CurrentValue、OriginalValue與IsModified屬性值:
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);
entry.Property(e => e.stor_name).CurrentValue = "New Eric the Read Books 1";
Console.WriteLine($" Name : {entry.Property(e => e.stor_name).Name }");
Console.WriteLine($" CurrentValue : {entry.Property(e => e.stor_name).CurrentValue }");
Console.WriteLine($" OriginalValue : {entry.Property(e => e.stor_name).OriginalValue }");
Console.WriteLine($" IsModified : {entry.Property(e => e.stor_name).IsModified }");
}
}
}
}
此範例執行結果如下,因為變動了stor_name屬性,因此IsModified的屬性值會是「true」,如此方能知會Entity Framework在叫用SaveChanges()方法儲存資料時,要產生Update的SQL語法:
Name : stor_name
CurrentValue : New Eric the Read Books 1
OriginalValue : Eric the Read Books
IsModified : True
除了使用強型別的Property()方法,傳入Lambda運算式來存取屬性之外,還有一個弱型別版本的Property()方法,可以傳入屬性的字串型別名稱,參考以下範例程式碼:
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);
entry.Property("stor_name").CurrentValue = "New Eric the Read Books 1";
Console.WriteLine($" Name : {entry.Property("stor_name").Name }");
Console.WriteLine($" CurrentValue : {entry.Property("stor_name").CurrentValue }");
Console.WriteLine($" OriginalValue : {entry.Property("stor_name").OriginalValue }");
Console.WriteLine($" IsModified : {entry.Property("stor_name").IsModified }");
}
}
}
}
找尋有修改的屬性
弱型別版本的Property()方法有一個好用的功能,可以用來找尋實體中所有有修改的屬性。參考以下範例程式碼先載入資料庫stores資料表stor_id為「6380」的記錄,叫用Entry()方法取得DbEntityEntry<store>物件,然後利用弱型別的Property()方法,傳入屬性名稱,設定stor_name屬性的值為「New Eric the Read Books 1」;city屬性的值為「Portland」,接著利用LINQ查詢,找尋IsModified屬性為「true」的屬性名稱:
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);
entry.Property("stor_name").CurrentValue = "New Eric the Read Books 1";
entry.Property("city").CurrentValue = "Portland";
var result = from n in entry.CurrentValues.PropertyNames
where entry.Property(n).IsModified
select n;
foreach (string name in result)
{
Console.WriteLine(name);
}
}
}
}
}
此範例執行結果如下:
stor_name
city
使用Reference()方法存取導覽屬性
Reference()方法可以用來找尋導覽屬性(Navigation Property),為了說明Reference()方法,我們將使用Employee實體做範例,Employee程式如下:
namespace PubsDemo
{
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.Spatial;
[Table("employee")]
public partial class employee
{
[Key]
[StringLength(9)]
public virtual string emp_id { get; set; }
[Required]
[StringLength(20)]
public virtual string fname { get; set; }
[StringLength(1)]
public virtual string minit { get; set; }
[Required]
[StringLength(30)]
public virtual string lname { get; set; }
public virtual short job_id { get; set; }
public virtual byte? job_lvl { get; set; }
[Required]
[StringLength(4)]
public virtual string pub_id { get; set; }
public virtual DateTime hire_date { get; set; }
public virtual job job { get; set; }
public virtual publisher publisher { get; set; }
}
}
Job實體程式如下:
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 job
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
public job()
{
}
[Key]
public virtual short job_id { get; set; }
[Required]
[StringLength(50)]
public virtual string job_desc { get; set; }
public virtual byte min_lvl { get; set; }
public virtual byte max_lvl { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<employee> employees { get; set; }
}
}
參考以下範例程式碼:
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 aEmployee = context.employees.Where(e => e.emp_id == "PMA42628M").Single();
DbEntityEntry<employee> entry = context.Entry(aEmployee);
entry.Reference(j => j.job).Load();
Console.WriteLine(entry.Reference(j => j.job).CurrentValue.job_desc);
}
}
}
}
範例先從資料庫取出emp_id為「PMA42628M」的員工資料,叫用Entry()方法取得DbEntityEntry< employee >物件,然後利用Reference()方法,存取job導覽屬性。Load()方法用來明確從資料庫載入相關聯的job資料。最後使用DbReferenceEntry<TEntity, TProperty>的CurrentValue印出相關聯的Job之job_desc值,此範例執行結果印出如下的值:
Sales Representative
修改導覽屬性
修改DbReferenceEntry<TEntity, TProperty>的CurrentValue的值,可以用來修改資料表與資料表之間資料的關聯性。參考以下範例程式碼,先從資料庫取出emp_id為「PMA42628M」的員工資料,叫用Entry()方法取得DbEntityEntry< employee >物件,然後利用Reference()方法,存取job導覽屬性。接著再下一個查詢,找尋Jobs資料表job_id為「12」的資料,最後將新job物件指定給Reference(j => j.job).CurrentValue屬性,並叫用SaveChanges()方法儲存異動到資料庫:
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 aEmployee = context.employees.Where(e => e.emp_id == "PMA42628M").Single();
DbEntityEntry<employee> entry = context.Entry(aEmployee);
entry.Reference(j => j.job).Load();
Console.WriteLine(entry.Reference(j => j.job).CurrentValue.job_desc);
var newJob = context.jobs.Where(j => j.job_id == 12).Single();
entry.Reference(j => j.job).CurrentValue = newJob;
Console.WriteLine(entry.Reference(j => j.job).CurrentValue.job_desc);
context.SaveChanges();
}
}
}
}
此範例執行結果如下所示:
Sales Representative
Editor
檢視資料庫,PMA42628M員工的job_id欄位值變更為「12」,請參考下圖所示:
圖 2:修改導覽屬性。
使用Collection ()方法存取集合型別導覽屬性
若導覽屬性的型別是一個集合,那麼你可以使用Collection ()方法存取集合類型的導覽屬性。更進一步,可以再利用DbCollectionEntry<TEntity, TElement>類別的CurrentValue屬性來讀寫相關聯的集合物件。
回顧一下store模型包含一個discounts導覽屬性關聯到discount:
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; }
}
}
Discount實體程式如下:
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 discount
{
[Key]
[Column(Order = 0)]
[StringLength(40)]
public virtual string discounttype { get; set; }
[StringLength(4)]
public virtual string stor_id { get; set; }
public virtual short? lowqty { get; set; }
public virtual short? highqty { get; set; }
[Key]
[Column("discount", Order = 1)]
public virtual decimal discount1 { get; set; }
public virtual store store { get; set; }
}
}
參考以下範例程式碼,先從資料庫取出stor_id為「6380」的商店資料,叫用Entry()方法取得DbEntityEntry< store >物件,然後利用Collection ()方法,存取discounts導覽屬性。從DbCollectionEntry<TEntity, TElement>類別的CurrentValue屬性取得集合,然後印出集合的Count筆數。
接著建立一個新discount物件,設定discounttype與discount,並加入context.discounts;最後叫用Add方法,將新建立的discount物件新增到entry.Collection(s => s.discounts).CurrentValue,並叫用SaveChanges()方法儲存異動到資料庫:
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);
entry.Collection(s => s.sales).Load();
int count = entry.Collection(s => s.discounts).CurrentValue.Count();
Console.WriteLine($" discount count : {count}");
discount newDiscount = new discount() { discounttype = "Special Discount", discount1 = 0.3m };
context.discounts.Add(newDiscount);
entry.Collection(s => s.discounts).CurrentValue.Add(newDiscount);
count = entry.Collection(s => s.discounts).CurrentValue.Count();
Console.WriteLine($" discount count : {count}");
context.SaveChanges();
}
}
}
}
此範例執行結果如下,資料表會新增一筆stor_id為「6380」的Special Discount記錄,請參考下圖所示:
圖 3:使用Collection ()方法存取集合型別導覽屬性。