Quantcast
Channel: ProgrammerXDB Blog - C#
Viewing all articles
Browse latest Browse all 73

Change Tracking API - 1

$
0
0

.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」鍵,請參考下圖所示:

clip_image002

圖 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」按鈕,請參考下圖所示:

clip_image004

圖 2:使用反向工程建立ADO.NET實體資料模型。

然後選取「Code First from database」,建立ADO.NET實體資料模型,請參考下圖所示:

clip_image006

圖 3:建立ADO.NET實體資料模型。

在下一個畫面,選取「New Connection」建立資料庫連接,請參考下圖所示:

clip_image008

圖 4:建立資料庫連接。

按「Next」按鈕進入到下一個畫面,以選取資料庫物件,請參考下圖所示:

clip_image010

圖 5:選取資料庫物件。

當精靈完成之後,專案中資料夾內將產生個多個C#檔案,模型架構請參考下圖所示:

clip_image012

圖 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字串,請參考下圖所示:

clip_image014

圖 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」狀態,請參考下圖所示:

clip_image016

圖 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」,請參考下圖所示:

clip_image017

圖 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

此範例執行結果請參考下圖所示:

clip_image019

圖 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,請參考下圖所示:

clip_image021

圖 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的型別參考如下圖所示:

clip_image023

圖 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();
            }
        }
    }
}


Viewing all articles
Browse latest Browse all 73

Latest Images

Trending Articles