2012年11月28日 星期三

ASP.NET Page-Life-Cycle

前言:
開發 ASP.NET Web Form 的工程師對於網頁生命週期(Page Life Cycle)一定要熟記在心中,也要知道每個循環中的會發生的個事件的主要目的,才不會在需求要修改或是出現Bug時不知道該往哪裡找問題。不過,講是這樣講其實ASP.NET的網頁生命週期需要注意的細節是滿多的,而且一個週期內會發生的事件可能比你想像中的還要多。

內文:
一般來說網頁生命週期會經過 "網頁要求"=>"開始"=>"初始化"=>"載入"=>"回傳事件處理"=>"呈現"=>"卸載"

階段 說明
網頁要求 在網頁生命週期開始前會發生網頁要求。當使用者要求網頁時,ASP.NET 會判斷是否需要剖析和編譯網頁 (因此開始網頁的生命週期),或是可以在不執行網頁的情況下,傳送網頁的快取版本做為回應。
開始 在開始階段,會先設定網頁屬性,如 Request 與 Response。在這個階段中,網頁也會判斷要求是否為回傳或是新的要求,然後設定 IsPostBack 屬性。網頁同時也會設定 UICulture 屬性。
初始化 在網頁初始化期間,可以使用網頁上的控制項,並且設定每個控制項的 UniqueID 屬性。如果適用,也會將主版頁面與佈景主題套用到網頁。如果目前的要求是回傳,則尚未載入回傳資料,並且控制項屬性值並未還原至檢視狀態提供的值。
載入 在載入期間,如果目前的要求是回傳,就會使用從檢視狀態和控制項狀態復原的資訊載入控制項屬性。
回傳事件處理 如果是回傳要求,會先呼叫控制項事件處理常式,然後才會呼叫所有驗證程式控制項的 Validate 方法,以設定個別驗證程式控制項與網頁的 IsValid 屬性。
呈現 在呈現前,會儲存網頁和所有控制項的檢視狀態。在呈現階段,網頁會呼叫每個控制項的 Render 方法,藉此提供文字寫入器將其輸出寫入網頁之 Response 屬性的 OutputStream 物件。
卸載 完整呈現網頁之後,會引發 Unload 事件,然後傳送至用戶端予以捨棄。此時將會執行網頁屬性 (如 Response 與 Request) 的卸載及清除工作。
資料來源 表一:MSDN 一般網頁生命週期階段 Page Life Cycle

而在這樣的生命週期階段中會有數十個事件依序發生,而不論是作為網頁容器的 Page 類別或是使用者控制項(User Control)或是伺服器控制項(Server Constrol)幾乎都有相對應的事件。
Page 頁面是每個 Web Form 都會繼承的類別,依據需求上面也可以放置自行撰寫的使用者控制項(User Control),或是裡面也可以直接擺上 Server Control,因此我們可以把這個類別是為一個容器,裡面可以擺放很多顯示用的控制項,而這個控制項裡會有很多事件會在一個 request 進來後依序引發,以下就是網頁生命週期的「生命週期事件」:

OnPreInit:
這個事件會在初始化之前就引發,通常會拿來檢查 PostBack 屬性、建立或是重建動態控制項、動態設定主版頁面、動態設定 Theme 屬性、讀取貨設定設定黨屬性值。

OnInit:
初始化所有控制項並套用任何面板設定後引發。個別控制項的 OnInit 事件會在容器前執行。可以在這個時候讀取或是初始化控制項的屬性。

OnInitComplete:
發生在網頁初始化結束時。

OnPreLoad:
這邊會於載入本身以及所包含的控制項的 ViewState,以及處理 request 執行個體中所附的回傳資料後引發。
若有自定驗證的話通常我會在這邊來進行,就不用載入太多東西。

OnLoad:
這邊容器會呼叫本身的 OnLoad 方法,然後以遞迴的方式對每一個控制項執行相同的動作。(因此控制項的 Load 會比較晚執行)通常我會於此設定控制項中的屬性,以及重建資料庫連線。

OnLoadComplete:
於 OnLoad 事件處理完畢後引發。

OnPreRender:
容器會呼叫自己的 PreRender 事件,然後也是以遞迴的方式對每一個控制項執行相同的動作。這個事件可以在呈現(Render)階段前對控制項的屬性做最後的變更。
通常我會在這邊把需要的 javascript 或 CSS 給引入或是對顯示邏輯進行修改。

OnSaveStateComplete:
這個事件是在儲存網頁所有控制項的 ViewState 後引發,因為是在呼叫Render方法之前,因此所做的任何變更依舊會影響控制項的呈現,但是因為 ViewState 已經儲存了,所以下一次的 PostBack 並不會擷取這些變更!
(因此PreRender才會被視為呈現前最後一個可以變更控制項的事件,因為此時變更後會被儲存下來!)

Render:
這不是個事件...在呈現這個階段每個控制項都會呼叫自己的Render方法來產生各自的HTML Tag。

OnUnload:
會先由控制項引發,控制項都引發完畢後才是容器引發。通常在這邊會對頁面使用到的特定資源進行釋放,像是與資料庫的連線或是關閉開啟的檔案。

以上資料來源 MSDN 生命週期事件 + 我的胡說八道

當然上面這些說明的文件看完之後還是需要手動來驗證一下才比較有感覺!
就讓我們來驗證一下吧!

驗證:

實驗一:
1)首先我們先新增一個空白ASP.NET Web Form專案,並且新增項目選擇Web Form,命名為Index.aspx。

2)打開 Package Manager Console 安裝NLog Install-Package NLog Install-Package NLog.Config
這邊安裝NLog是為了用來記錄每個事件的觸發順序使用。

3)修改NLog.config
<targets>
    <target xsi:type="File" name="test" fileName="${basedir}/logs/${shortdate}.log"
            layout="${longdate} ${message}"/>
  </targets>

  <rules>
    <logger name="test" minlevel="Trace" writeTo="test" />
  </rules>

4)在Index.aspx.cs中寫下日誌
using System;
using NLog;
public partial class Index : System.Web.UI.Page
{
    private Logger _logger = LogManager.GetLogger("test");

    protected override void OnPreInit(EventArgs e)
    {
        _logger.Trace("OnPreInit event execute!");
        base.OnPreInit(e);
    }

    protected override void OnInit(EventArgs e)
    {
        _logger.Trace("OnInit event execute!");
        base.OnInit(e);
    }

    protected override void OnInitComplete(EventArgs e)
    {
        _logger.Trace("OnInitComplete event execute!");
        base.OnInitComplete(e);
    }

    protected override void OnPreLoad(EventArgs e)
    {
        _logger.Trace("OnPreLoad event execute!");
        base.OnPreLoad(e);
    }

    protected override void OnLoad(EventArgs e)
    {
        _logger.Trace("OnLoad event execute!");
        base.OnLoad(e);
    }

    protected override void OnLoadComplete(EventArgs e)
    {
        _logger.Trace("OnLoadComplete event execute!");
        base.OnLoadComplete(e);
    }

    protected override void OnPreRender(EventArgs e)
    {
        _logger.Trace("OnPreRender event execute!");
        base.OnPreRender(e);
    }

    protected override void OnPreRenderComplete(EventArgs e)
    {
        _logger.Trace("OnPreRenderComplete event execute!");
        base.OnPreRenderComplete(e);
    }

    protected override void OnSaveStateComplete(EventArgs e)
    {
        _logger.Trace("OnSaveStateComplete event execute!");
        base.OnSaveStateComplete(e);
    }

    protected override void OnUnload(EventArgs e)
    {
        _logger.Trace("OnUnload event execute!");
        base.OnUnload(e);
        _logger.Trace("-------------------");
    }
}

5)按下F5成功執行後直接關起來

6)觀察寫下來的Log檔

這邊我們可以發現頁面的事件順序大致上就是先初始化、載入、呈現最後就是卸載。
但若我們在頁面上面多加一個使用者控制項(User Constrol)呢?

實驗二:

1)在原本的專案中新增一個ascx檔,並且附上一個 Server Control。

2)新增如下內容
<asp:TextBox ID="ucTextBox1" runat="server" 
             OnInit="ucTextBox1_Init"
             OnLoad="ucTextBox1_Load"
             OnPreRender="ucTextBox1_PreRender"
             OnUnload="ucTextBox1_Unload"></asp:TextBox>
public partial class UserControl1 : System.Web.UI.UserControl
{
    private Logger _logger = LogManager.GetLogger("test");

    protected override void OnInit(EventArgs e)
    {
        _logger.Trace("User Control OnInit event execute!");
        base.OnInit(e);
    }

    protected override void OnLoad(EventArgs e)
    {
        _logger.Trace("User Control OnLoad event execute!");
        base.OnLoad(e);
    }

    protected override void OnPreRender(EventArgs e)
    {
        _logger.Trace("User Constrol OnPreRender event execute!");
        base.OnPreRender(e);
    }

    protected override void OnUnload(EventArgs e)
    {
        _logger.Trace("User Constrol OnUnload event execute!");
        base.OnUnload(e);
    }

    protected override void OnDataBinding(EventArgs e)
    {
        _logger.Trace("User Control OnDataBinding event execute!");
        base.OnDataBinding(e);
    }

    #region 伺服器控制項的事件
    protected void ucTextBox1_Unload(object sender, EventArgs e)
    {
        _logger.Trace("ucTextBox1 Unload event execute!");
    }

    protected void ucTextBox1_PreRender(object sender, EventArgs e)
    {
        _logger.Trace("ucTextBox1 PreRender event execute!");
    }

    protected void ucTextBox1_Load(object sender, EventArgs e)
    {
        _logger.Trace("ucTextBox1 Load event execute!");
    }

    protected void ucTextBox1_Init(object sender, EventArgs e)
    {
        _logger.Trace("ucTextBox1 Init event execute!");
    } 
    #endregion
}

3)按下F5成功執行後直接關起來

4)觀察日誌檔

這邊我們可以觀察到初始化時會是控制項的初始化先進行,然後才是 User Control 的初始化,最後是 Page 的初始化(容器的初始化會比較晚)。
而在載入的時候會先由 Page 先行載入,然後是 User Control 的載入,最後才是伺服器控制項的載入(容器會先行載入)。
而在卸載的時候則是控制項先卸載,然後是 User Constrol,最後才是 Page(容器的卸載的比較晚)。

實驗三:
現在我們在 Page 頁面也新增兩個 button 來觀察看看各控制項的事件引發順序。

1)我們分別在使用者控制項(User Control)的前後增加一個伺服器控制項(Server Control)。現在Index.aspx就會變成下面這樣:
<form id="form1" runat="server">
    <div>
        <asp:Button ID="button1" runat="server" Text="Firstbutton" 
                    OnInit="button1_Init"
                    OnLoad="button1_Load"
                    OnPreRender="button1_PreRender"
                    OnUnload="button1_Unload" />
        <uc:UserControl1 ID="uc1" runat="server" Visible="true" />
        <asp:Button ID="button2" runat="server" Text="Secondbutton" 
                    OnInit="button2_Init"
                    OnLoad="button2_Load"
                    OnPreRender="button2_PreRender"
                    OnUnload="button2_Unload" />
    </div>
</form>

2)在兩個 button 的事件中寫下日誌...方法就跟上面的寫法一樣...
#region 伺服器控制項的事件
    protected void button1_Init(object sender, EventArgs e)
    {
        _logger.Trace("Fisrtbutton Init event execute!");
    }

    protected void button1_Load(object sender, EventArgs e)
    {
        _logger.Trace("Fisrtbutton Load event execute!");
    }

    protected void button1_PreRender(object sender, EventArgs e)
    {
        _logger.Trace("Fisrtbutton PreRender event execute!");
    }

    protected void button1_Unload(object sender, EventArgs e)
    {
        _logger.Trace("Fisrtbutton Unload event execute!");
    }
    protected void button2_Init(object sender, EventArgs e)
    {
        _logger.Trace("Secondbutton Init event execute!");
    }

    protected void button2_Load(object sender, EventArgs e)
    {
        _logger.Trace("Secondbutton Load event execute!");
    }

    protected void button2_PreRender(object sender, EventArgs e)
    {
        _logger.Trace("Secondbutton PreRender event execute!");
    }

    protected void button2_Unload(object sender, EventArgs e)
    {
        _logger.Trace("Secondbutton Unload event execute!");
    } 
#endregion

3)按下F5之後直接關掉觀察日誌檔...

初始化>>
這邊我們可以觀察到初始化的時候會依照控制項出現的順序依序初始化,因此第一個 button 會先被初始化,然後是使用者控制項中的 textbox 初始化,接下來就是使用者控制項的初始化!接下來才是第二個 button 的初始化,最後才是 Page 頁面(基底容器) 的初始化。(個別控制項的初始化會在它的容器前執行)

載入>>
載入時依據MSDN官方說法是容器會先控制項載入,我們來驗證吧~!這邊我們可以看到 Page 頁面的載入確實先觸發了!然後是第一個 button 的載入,接下來是使用者控制項的載入,接著使用者控制項中的 textbox 載入,最後是第二的 button 的載入。載入就一這樣的順序結束了!

呈現(Render)>>
這邊很特別,我們可以看到這個 Render 的順序根本就是頁面從上到下的順序依序呼叫 PreRender,不過也難怪啦~畢竟這邊是要產生 HTML Tag 是最後要呈現在瀏覽器的樣子,會有這樣的行為其實不意外,要是順序不是這樣最後呈現可能也會怪怪的。

卸載>>
卸載這邊則會讓控制項先行卸載,最後才是容器卸載。所以這邊第一個 button 就會先執行卸載,接下來是使用者控制項中的 textbox 卸載,接著是使用者控制項,然後是第二個 button,這些控制項都卸載後最後才是 Page 頁面的卸載。

結語:
在撰寫 ASP.NET Web Form 時了解網頁生命週期是不可或缺的知識,他可以幫助你理解 Web Form 架構時的一些眉角,能夠在正確的時間點做正確的事情。雖然現在在強大的 javascript library 或 framework 的幫助下,已經讓網頁的使用者體驗愈趨近 desktop 導致 Web Form 的式微,但是偶而還是會有需要使用到的時候,到時候就會需要了解這方面的知識了。

參考資料:
MSDN
ASP.NET Page Life Cycle Overview
dotblog
In 91 [ASP.NET]Page Lify Cycle整理
m@rcus學習筆記 [.NET]ASP.NET網頁生命週期Page Life-Cycle

2012年11月20日 星期二

Entity Framework Migrations - 入門使用

前言:
使用 Entity Framework 的 Code First 來做資料存取層與既存資料庫溝通時多少都會遇到資料庫 Schema 變更的時候,只是這個時候若是有用到 Code First 來新增表格的話就會遇到像上一篇一樣的問題;執行時期使用資料庫的話會引發錯誤訊息,而且會通知因為資料庫的 Schema 變更需要使用 Migrations 來解決問題。因此這篇文章就是要來說明一般來說使用 Code First 來開發遇到資料庫 Schema 要變更時的正規作法應該怎麼做。

內文:
我們就用上一篇所生出來的資料庫來實作看看吧~


不過為了方便起見,我們就直接先把 Products 這個資料表先刪除,我們來看看用 Code First 的方式開發的話若要新增一個資料表的正規方式是什麼?(這邊正規也只是我在講而已,只是這麼做的話可以免除之後的麻煩)
首先我們要先打開套件管理員的命令列視窗「Tools => Library Package Manager => Package Manager Console」,然後要用 Migrations 的話就要先把功能打開,需要下這樣的指令:「PM >> Enable-Migrations」,成功的同時我們的專案會新增一個Migrations資料夾,並且會給你一個 Configrations.cs 類別,若沒有要使用自動Migrations的話就可以先不用動這個類別的內容。接下來因為需求異動的關係我們需要多一個 Products 資料表來儲存產品的資訊。Products 的資料表如下
/// 
/// 產品的實體。
/// 
[Table("Products")]
public class Product
{
    /// 
    /// 產品的識別號。
    /// 
    [Key]
    [ScaffoldColumn(false)]
    public int ID { get; set; }
    /// 
    /// 產品的名稱。
    /// 
    public string Name { get; set; }
    /// 
    /// 產品的價格。
    /// 
    public double Price { get; set; }
    /// 
    /// 生產產品的工廠名稱。
    /// 
    public string FactoryName { get; set; }
}

對於資料庫的 Schema 的異動我們可以在 Package Manager Console 中輸入 Add-Migrations xxxxxx
xxxxxx就是新增的類別的名稱,通常會是這次異動的表格、欄位的名稱像是AddProducts之類的。這邊我是用AddTableProducts
using System;
using System.Data.Entity.Migrations;

public partial class AddTableProducts : DbMigration
{
    // Operations to be performed during the upgrade process.
    public override void Up()
    {
    }

    // Operations to be performed during the downgrade process.
    public override void Down()
    {
    }
}
使用 Migrations 時一定要實作這兩個方法,而且內容要互相對應。
什麼意思呢?就是說若今天是要新增一個 Table 的話,那我們就要在 Up() 中放置新增 Table 的程式,而在 Down() 中放置捨棄該 Table 的程式像是底下這樣:
public partial class AddTableProducts : DbMigration
{
    public override void Up()
    {
        // 新增一個資料表就呼叫CreateTable方法,需指名資料表明稱,以及資料表各欄位的資料型態
        CreateTable(
            "dbo.Products",
            c => new
                {
                    ID = c.Int(nullable: false, identity: true),
                    Name = c.String(),
                    Price = c.Double(nullable: false),
                    FactoryName = c.String(),
                })
            .PrimaryKey(t => t.ID);
            
    }
        
    public override void Down()
    {
        // Down 這邊要放是 CreateTable的相反方法,這邊就是 DropTable方法
        DropTable("dbo.Products");
    }
}
寫完之後我們在 Package Manage Console 中下個 Update-Database 指令,順利執行完畢之後我們去資料庫中就可以看到新增後的資料表了~
至於新增一筆欄位我們也可以如法炮製,若我們現在 Products 資料表又多了一筆欄位的需求,要把產品的庫存數量記錄下來,首先我們先在 Model 中修改新的 Products 資料表的 Schema (我們在最後面新增了一個屬性"Quantity"):
/// 
/// 產品的實體。
/// 
[Table("Products")]
public class Product
{
    /// 
    /// 產品的識別號。
    /// 
    [Key]
    [ScaffoldColumn(false)]
    public int ID { get; set; }
    /// 
    /// 產品的名稱。
    /// 
    public string Name { get; set; }

    /// 
    /// 產品的價格。
    /// 
    public double Price { get; set; }

    /// 
    /// 生產產品的工廠名稱。
    /// 
    public string FactoryName { get; set; }

    /// 
    /// 產品於庫存的數量。
    /// 
    public int Quantity { get; set; }
}
好哩~這樣Code就算改好了,接下來我們只要下個確認異動的指令讓資料庫異動就好了~
我們在 Package Manage Console 中鍵入「Update-Database」等待執行完畢我們就可以去資料庫確認一下異動是否成功了(其實只要 Console 沒有吐出例外訊息應該都是沒有問題的)

噹~噹~Great!資料庫果然真的有這麼一個欄位在Products資料表中生出來了!
接下來在執行時期也可以很放心的不會吐出任何例外訊息因為資料庫異動而要你去 Migrations 了。
我們也可以來順便看看使用 Migrations 時會在 Demo 資料庫中的 Products 資料表的系統資料表產生的 _Migrations 資料表的內容。
我們這個範例中總共做了 Migrations 兩次,一次是新增資料表,一次是新增欄位:
嗯~果然在這個資料表中我們也發現了兩筆資料。

小結:
在使用 Entity Framework 的 Code First 時對開發人員真的是很方便,以前需要很麻煩的資料庫與程式碼互相切換,還要學會怎麼寫 SQL 這些麻煩事情,使用 Entity Framework 這種 ORM solution 都可以解決掉。而且這次 EF 所提供的 Migrations 功能又可以讓開發人員直接在程式碼中對資料庫異動,完全不需要離開舒適的開發環境。說真的,現在開發要與資料庫互動而且是 MS SQL 那目前的最佳方案我想就是 EF 的 Code First 了!

2012年11月16日 星期五

Code First with existiong db - 手殘問題

前言:
話說之前那一篇的 Entity Framework 觀念其實還是有很多地方是不大清楚的,像是使用 Code First with existing database 時什麼情況會異動到資料庫?什麼情況下需要做Migrations?什麼情況下會直接吐個 Exception 給你而不是自動幫你多個 Table 或 Column?,所以今天就趁著一點時間來寫一下從接觸至今的處理經驗了...(說的好像經驗老到其實還是個新手)

前置工作:
這次的範例中我們會需要一個無辜的資料庫讓我們(恣意蹂躪)測試。
就讓我們把這個偉大的資料庫命名為...Demo!

唔...其實Products先不用建起來,我們可以來做個實驗看看什麼時候會觸發Migrations...好~建立起這個資料庫之後我們就可以馬上來寫Code玩玩看啦~!
這裡我們就使用 Console Application 就好。我們建立一個名為 EFMigrationDemo 的專案,並在根目錄處新增一個 Model 資料夾裡面放置我們最重要的類別。
在寫 Code 之前我們要先把 EntityFramework 裝起來~就直接打開 Package Manager Console 吧!請在 Console 中直接輸入:
Install-Package EntityFramework 若不放心是否為最新就接著下:
Update-Package EntityFramework 這樣就可以保證你所指定的套件是最新的!
前置工作這樣就算是告一段落了~

寫Code囉:
我們新增一個類別檔案於 Model 資料夾中
using System.Data.Entity;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace EFMigrationDemo.Model
{
    public class Customers // 類別名稱與資料庫中的表格名稱一樣
    {
        // 標記該屬性(在資料庫中為欄位)為 Promary key
        //(若屬性結尾為 ID 依照 Conversion naming 規則 EF 會自己判斷出來)
        [Key]
        public int ID { get; set; }

        // 標記該欄位是不能為Null的
        [Required]
        public string Name { get; set; }

        [Required]
        public string Address { get; set; }

        [Required]
        public string PhoneNumber { get; set; }

        [Required]
        public string Email { get; set; }

        // 標記這個屬性對應到資料庫中的欄位名稱為 NickName 而非 TheNickName
        // 而且該欄位的內容長度不超過50
        [Column("NickName")]
        [MaxLength(50, ErrorMessage = "Your Nick Name too long")]
        public string TheNickName { get; set; }
        
        // 標記這個屬性並不會對應到資料庫中的任何欄位
        [NotMapped]
        public string NameplusNickName { get { return Name + NickName; } }

        // 這是個類別所以還是可以有方法,而且不會被認為是欄位或是預儲程序
        public void PrintName() { Console.WriteLine("My name is {0}", this.Name); }
    }

    // 繼承 DbContext 的類別可以當成是資料庫來進行操作,預設會去 Config 檔中抓取 connectionString 的 section 裡與類別名稱相同的項目,像是:
    // <add name="Demo" connectionString="" providerName="">
    // 而類別名稱其實也可以不用跟資料庫名稱一樣
    public class Demo : DbContext
    {
        // 被 DbSet 包起來的類別會被判定為是一個實體(也就是一個表格)
        // 所以屬性名稱並不會影響到表格名稱
        public DbSet<Customers> Customer { get; set; }
    }
} 

這樣我們的 Model 類別就算準備好了,接下來就是準備呼叫端了~
using EFMigrationDemo.Model;
        
        static void Main(string[] args)
        {
            Program p = new Program();
            p.Run();
            Console.WriteLine("Process Run has been finished");
            Console.Read();
        }

        void Run()
        {
            Console.WriteLine("Code First begining...");
            using (Demo db = new Demo())
            {
                var result = (from p in db.Customers
                              select p).FirstOrDefault();
                if (result != null)
                    Console.WriteLine("Customer Name is {0}", result.NameplusNickName);
            }
        }

執行之後就是會印出來資料庫內的第一筆資料哩~
以上就是很順利的 Code First 運作情況,當然事事不會都這麼順利又美好的,若我Customers類別名稱跟資料庫不同的話呢?像是這樣:
    public class MyCustomers
    {
        [Key]
        public int ID { get; set; }

        [Required]
        public string Name { get; set; }

        [Required]
        public string Address { get; set; }

        [Required]
        public string PhoneNumber { get; set; }

        [Required]
        public string Email { get; set; }

        [Column("NickName")]
        [MaxLength(50, ErrorMessage = "Your Nick Name too long")]
        public string TheNickName { get; set; }
        
        [NotMapped]
        public string NameplusNickName { get { return Name + NickName; } }
    }
    
    
    public class Demos : DbContext
    {
        public DbSet<Mycustomersgt; Customer { get; set; }
    }

重新建置後再次執行Program.cs的內容!Console會印出:
Code First begining...
Process Run has been finished
沒了...但是讓我來看看DB有什麼變化吧
噹~噹~!
恭喜你剛剛用 Code First 新建一個 Table 了耶!等等...這不是你要的效果嗎?! Entity Framework 一臉疑惑的問到
這時候你才發現到你剛剛手殘不小心把Table名稱打錯了,改快把名稱改回來重新建置一次,建置成功!讓我們再Run一次吧~
你被殘酷的拒絕了...不過錯誤訊息很貼心的說你應該要用Migration來整合資料庫,因為已經不大一樣了。停!它怎麼知道資料庫不一樣了?
不要急...讓我們看看這張圖吧你就知道為什麼它會知道資料庫被動過了
喔!什麼時候在系統資料表中新增了一個_MigrationHistory資料表了?!而且裡面還有一筆資料!
唔...這就是用 Code First 異動資料庫時它生出來的東西,在這種情況下要重新 run 時就請用 Migration 整合後再說吧!
不過先讓我們看看另外一種錯誤吧!

當我們在撰寫 Customers 類別時實在太粗心了,不小心多加了一個不屬於這個資料表的欄位(假設屬性名稱叫NotExist)也就是說多了一個屬性,而且也沒有貼上[NotMapped]標籤,就直接很開心的 ctr+Alt+b 然後 F5 就下去了。依據經驗法則是不是會在資料庫中多一個欄位呢?讓我們看看吧~
嗯...是個錯誤訊息,表明了這欄位在 Customers 中找不到,所以放心~資料庫中的 Customers 表格也沒有多一個欄位。

最後(ps.Migration怎麼用就留到下一篇吧),預設上 DbContext 會去找跟類別名稱相同的連線字串名稱,但是其實是可以自訂的,這需要參考Syste.Data.Entity.dll。實際方式像是這樣:
public class Demos : DbContext
    {
        public DbSet<customers> Customer { get; set; }
        
        // 這裡呼叫 DbContext 的建構子並且給予一個字串,這個字串就是所指定的連線字串名稱
        // 若有給定的話就不會用預設的方式去找連線字串名稱了
        public Demos() : base("Demo") { }
    }

呼~這篇累積了好多 bug 沒有修啊,但是我打字有點累了...就下回待續吧 XD

2012年11月8日 星期四

EF with Repository Pattern - 簡易版

情境:
上一篇中我們有使用到一個NorthwindReposity類別來存取資料庫,不過我們也發現到這樣的寫法與呼叫端的耦合度還是有點高。若我要使用的資料所要做的事情是固定的,像是瀏覽產品資訊時的產品資料是從主資料庫中找到產品的資料表並取出指定產品的資料,那麼其實對這個部分來說其實他就只關心產品得資料如何存取和操作而已。一個簡單的想法是,我們讓呼叫端用一個介面來針對他所感興趣的部份去做操作。

實作:
我們用的資料庫依然是北風資料庫,而且和上一篇是一樣的!避免頁面切來切去這邊就直接寫囉~
using System.Data.Entity;
System.ComponentModel.DataAnnotations.Schema;
public class Northwind : DbContext
{
    public DbSet<product> Products { get; set; }

    public DbSet<category> Categories { get; set; }
}
public class Product
{
    public int ProductID { get; set; }
    public string ProductName { get; set; }
    public Decimal? UnitPrice { get; set; }
    public bool Discontinued { get; set; }
    [ForeignKey("Category")]
    public int CategoryID { get; set; }

    public virtual Category Category { get; set; }
}
public class Category
{
    public int CategoryID { get; set; }
    public string CategoryName { get; set; }
    public string Description { get; set; }
    public byte[] Picture { get; set; }

    public virtual ICollection<product> Products { get; set; }
}
接下來就是我們上一篇用來存取資料庫的主要類別,也是我們這次的修改標的物。
public class NorthwindRepository : IDisposable
    {
        private Northwind _db;
        
        public NorthwindRepository()
        {
            _db = new Northwind();
        }

        public ICollection<string> ListAllProductName()
        {
            return _db.Products.Select(a => a.ProductName).ToList();
        }

        public ICollection<string> ListAllCategoryName()
        {
            return _db.Categories.Select(a => a.CategoryName).ToList();
        }

        public void Dispose()
        {
            if (_db != null)
                _db.Dispose();
        }
    }
依照先前所說,我們先來做一個讓呼叫端使用的介面吧~我們這邊是以Product資料為例子
public interface IProductRepository
{
     IList<Product> GetProducts();
     Product GetProductByID(int productId);
     Product GetProductByName(string productName);
     void InsertProduct(Product product);
     void DeleteProduct(int productId);
     void UpdateProduct(Product porduct);
}
接著我們要寫一個實作這個介面的類別,並且讓他實際去取出資料。
public class ProductReposity : IRepository
{
     private Northwind _ db = null;
     public ProductReposity(Northwind context)
     {
          if(context == null) throw new ArgumentNullException("context");
          this._db = context;
     }
     public IList<Product> GetProducts()
     {
          return _db.Products.ToList();
     }
     public Product GetProductByID(int productId)
     {
          return _db.Products.Where(a => a.ProductID == productId).Select(a => a).FirstOrDefault();
     }
     public Product GetProductByName(string productName)
     {
          return _db.Products.Where(a => a.ProductName == productName).Select(a => a).FirstOrDefault();
     }
     public void InsertProduct(Product product)
     {
          _db.Products.Add(product);
          _db.SaveChanges();
     }
     public void DeleteProduct(int productId)
     {
          Product product = _db.Products.Find(productId);
          _db.Products.Remove(product);
          _db.SaveChanges();
     }
     public void UpdateProduct(Product product)
     {
          Product target = _db.Products.First(a => a.ProductID == product.ProductID);
          target = product;
          _db.SaveChanges();
     }
以上~在呼叫端時我們就可以這樣使用:
IProductRepository db = new ProductRepository(new Northwind());
var items = db.GetProducts();
foerach(var p in items)
{
     Console.WriteLine("Product Name {0}", p.ProductName);
}
這樣我們就進一步的把呼叫端跟資料庫分離開一點了,不過寫到這邊就打住的話實在會很虛...那我們就把這個情境稍微擴大,變成「有多個資料的操作都是類似的」。依照這樣的寫法我們需要新增一個IRepository類別和與之相對應的Repository類別,既然操作都是類似的能不能介面只寫一份就好了呢?可以的!那我們就來改改吧~
public interface IRepository<T> : IDisposable
{
     IList<T> GetProducts();
     T GetProductByID(int itemId);
     T GetProductByName(string itemName);
     void InsertProduct(T item);
     void DeleteProduct(int itemId);
     void UpdateProduct(T item);
}
而把ProductRepository改成如下:
public class ProductReposity : IRepository<Product>
{
     // 判定是否已經呼叫Dispos()方法。
     private bool disposed = false;

     private Northwind _ db = null;
     public ProductReposity(Northwind context)
     {
          if(context == null) throw new ArgumentNullException("context");
          this._db = context;
     }
     public IList<Product> GetProducts()
     {
          return _db.Products.ToList();
     }
     public Product GetProductByID(int productId)
     {
          return _db.Products.Where(a => a.ProductID == productId).Select(a => a).FirstOrDefault();
     }
     public Product GetProductByName(string productName)
     {
          return _db.Products.Where(a => a.ProductName == productName).Select(a => a).FirstOrDefault();
     }
     public void InsertProduct(Product product)
     {
          _db.Products.Add(product);
          _db.SaveChanges();
     }
     public void DeleteProduct(int productId)
     {
          Product product = _db.Products.Find(productId);
          _db.Products.Remove(product);
          _db.SaveChanges();
     }
     public void UpdateProduct(Product product)
     {
          Product target = _db.Products.First(a => a.ProductID == product.ProductID);
          target = product;
          _db.SaveChanges();
     }
     // 用戶端主動呼叫表明要釋放資源的函式。
     public void Dispose()
     {
         if (_db != null)
             this.Dispose(true);
         //通知GC不用呼叫 finalize 來釋放資源。
         GC.SuppressFinalize(this);
     }
     /// <remarks>
     /// 無法被外部呼叫。
     /// 若是true表示是被用戶端呼叫,manage resource和unmanage resource都可以釋放。
     /// 若是false表示是被GC呼叫,此時應該只釋放unmanage resource。
     /// </remarks>
     protected virtual void Dispose(bool disposing)
     {
         if (!disposed)
         {
             if (disposing)
             {
                 _db.Dispose();
             }
             disposed = true;
         }
     }

     /// <summary>
     /// finalizer。這個無法自行呼叫是由GC來使用的。
     /// </summary>
     ~ProductReposity()
     {
         //以false告知Dispose函數是從垃圾回收器(GC)在呼叫Finalize。
         Dispose(false);
     }
}
ps(這邊的Dispose()寫法的好處是,若呼叫端忘記呼叫Dispose()方法還有Finalize讓GC可以幫忙把物件使用的manage/unmanage resource回收,若呼叫端呼叫Dispose()方法時可以即時把資源進行回收動作,而且不用GC又呼叫一次可以比較有效率點。)
好哩~這樣新的IRepository介面就套用成功了!其實沒什麼變嘛~不過...這樣的話之後若對資料操作的邏輯是相似的話用同一個介面就好了,不需要產生那麼多的介面,也算是省了那麼一點工吧(?) XD