2012年11月21日 星期三

ASP.Net MVC中的Model

image

        打開MVC預設範例的Models資料夾可以看到內部僅有一個副檔名為CS的檔案。依照我們先前所說,MVC中的Model其實很單純,它僅將眼界放在自己的任務上,它不會去理會其它Model或是Controller,對於自己的職責範圍它總是盡力作到,而外面的世界與它全然無關。正因為這樣的特性,我們可以說Model本身是一個獨立的最小單元,它僅負責它自己該負責的部份,它不會涉入非它責任範圍的任何事項。


       在Model的工作中,我們可以簡單歸納成下列幾項:
  • 資料存取
  • 商業邏輯

      在第一項資料存取上,Model對於任何資料的存取或是擷取都是由它負責來完成,可別小看這項工作,有的時候它可能需要從遠端伺服器上下載一份文件檔或是傳上文件檔。大多的範例都是以資料庫的資料存取作為範例,不諱言,這的確也是MVC網站絕大部份時間所需要去做的事情。也因此,在網路上查詢MVC的Model時總是會在網路上找到許多有關資料庫存取的相關文章,其中較多的就是微軟努力在推行的ORM框架:Entity Framework。

    Entity Framework目前已經推行到Entity Framework 5了,這款微軟自家的ORM框架相較於由Java轉生過來的NHibernate來說是比較輕量級一點的,但是功能上當然也比NHibernate少了一些。總的來說,其實我個人還是挺看好微軟這套ORM框架的,除了它有特定的團隊在維護升級之外,它提供了許多誘人的特性,如下列:
  • Migration
  • 支援三種模式: Database First, Model First, Code First
  • Linq語法
  • Entity Framework 5在效能上的提升(有環境上的限制)
  • Data Annotation
  • Entity Relation的Fluent設定

   Entity Framework在Migration上做得很徹底,而這也是我很喜歡的一個功能,在過去開發有關資料庫的系統,如果資料表欄位需要作任何異動,對於系統來說都是一件麻煩的事情;首先,你必須要進入Database中並且進行資料表的修改,這是一件令人非常頭痛的事情。在Entity Framework上,倘若目前仍處理開發設計階段,僅需在Package Console中下幾行指令,就能很快速又簡單的修改資料表的欄位,這真的是一件利多。(我的筆電很爛,通常開了Visual Studio又要再開啟SSMS,這個時候記憶體就不太夠了)

      在前幾代的Entity Framework僅支援Database First這種特性,但是隨著版本的演進慢慢的開始支援了Model First;亦即,開發人員可以直接在Visual Studio拉模型到設計區塊中,並且設定好一些屬性就能夠快速建立及產生一個資料庫的所有資料表,這在當時的確相當驚人,因為這改變了過去系統的開發流程,從先進資料庫設定一堆資料表到了現在可以從Visual Studio中設定。但這樣仍無法滿足所有的開發人員。很多時候如果系統並不是那麼巨型,且客戶要求的時程非常的短促,這通常會將許多SA和SD的時間壓縮,會很迫切的需要快速轉入實做中,若這個時候還需要在那邊慢慢拉模型,這對於性子較急的開發人員或專案經理來說,簡直就是地獄

       除了時程問題之外,考慮另一個議題:複用性。許多時候,團隊在開發系統上會採用領域模型分析,亦即,會先將本次要開發的系統其最關鍵核心的部份建構出Domail Model(領域模型),而這個領域模型可能在下一次的專案或是延伸專案中會被使用到,這便產生了複用的需求,在過去的Entity Framework中,無論是Database First抑或是Model First都無法滿足此類要求,因為這些模式的產物很難完全移轉到其它專案身上,縱使可以移轉也需要再進行一番加工才行。為此,Code First這種模式產生了!

      Code First一如其名,這種模式就是什麼都不用管,直接看著Domail Model圖將上面的領域物件寫成一個個的類別到系統中,不需要先進資料庫也不需要打開Entity Framework專屬的設計畫面,只要很簡單的新增幾個類別就結束了。接下來我們就來實際欣賞MVC專案中Code First的真實樣貌。

     打開AccountModels.cs這個檔案,其內容如下:
 
namespace MvcApplication1.Models
{
    public class UsersContext : DbContext
    {
        public UsersContext()
            : base("DefaultConnection")
        {
        }

        public DbSet UserProfiles { get; set; }
    }

    [Table("UserProfile")]
    public class UserProfile
    {
        [Key]
        [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
        public int UserId { get; set; }
        public string UserName { get; set; }
    }

    public class RegisterExternalLoginModel
    {
        [Required]
        [Display(Name = "使用者名稱")]
        public string UserName { get; set; }

        public string ExternalLoginData { get; set; }
    }

    public class LocalPasswordModel
    {
        [Required]
        [DataType(DataType.Password)]
        [Display(Name = "目前密碼")]
        public string OldPassword { get; set; }

        [Required]
        [StringLength(100, ErrorMessage = "{0} 長度至少必須為 {2} 個字元。", MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "新密碼")]
        public string NewPassword { get; set; }

        [DataType(DataType.Password)]
        [Display(Name = "確認新密碼")]
        [Compare("NewPassword", ErrorMessage = "新密碼與確認密碼不相符。")]
        public string ConfirmPassword { get; set; }
    }

    public class LoginModel
    {
        [Required]
        [Display(Name = "使用者名稱")]
        public string UserName { get; set; }

        [Required]
        [DataType(DataType.Password)]
        [Display(Name = "密碼")]
        public string Password { get; set; }

        [Display(Name = "記住我?")]
        public bool RememberMe { get; set; }
    }

    public class RegisterModel
    {
        [Required]
        [Display(Name = "使用者名稱")]
        public string UserName { get; set; }

        [Required]
        [StringLength(100, ErrorMessage = "{0} 長度至少必須為 {2} 個字元。", MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "密碼")]
        public string Password { get; set; }

        [DataType(DataType.Password)]
        [Display(Name = "確認密碼")]
        [Compare("Password", ErrorMessage = "密碼和確認密碼不相符。")]
        public string ConfirmPassword { get; set; }
    }

    public class ExternalLogin
    {
        public string Provider { get; set; }
        public string ProviderDisplayName { get; set; }
        public string ProviderUserId { get; set; }
    }
}
在上述程式碼中,一共有7個類別在裡面,其中第2個是在定義資料表,而最上面那一個是Entity Framework中一個很重要的類別。
    (在一般系統開發慣例來說,通常是不會把這麼多個類別塞在一個檔案中,大多時候都是一個類別一個檔案)

   這下可有趣了,這麼多個類別怎麼只有一個類別是在定義資料表,而其它類別呢?它們又是在做些什麼用途?我們先來看看前面兩個類別在做些什麼,再來討論下面那些類別。
    [Table("UserProfile")]
    public class UserProfile
    {
        [Key]
        [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
        public int UserId { get; set; }
        public string UserName { get; set; }
    }

這個類別所定義的資料表很簡單,它僅有兩個欄位: UserId以及UserName。其中比較特別的部份是UserId的上頭有兩個標籤屬性,一者為[Key]而另一個則看起來複雜的樣子。其實望文生義可以很快速的瞭解到,這兩個標籤在說明UserId對於這張表來說是主索引鍵,而第二個標籤在說明它的值是自動產生的,而產生出來的值會依照其屬性的資料型別int作值的遞增;也就是說,第一筆資料的UserId是0而第二筆是1如此類推之。

      類別上的標籤屬性就更加讓人容易明白,它就很直白的說明這是一張資料表,而其資料表的名稱為UserProfile(註:資料表的名稱可以和類別名稱不一樣)。這些標籤屬性其實有個專有名詞: Data Annotation
    public class UsersContext : DbContext
    {
        public UsersContext()
            : base("DefaultConnection")
        {
        }

        public DbSet UserProfiles { get; set; }
    }

任何一個ORM的框架都勢必會有一個本文(Context),大家可以把它當作成一個容器,一個系統可以有多個Context但是這些Context彼此互相獨立,其目的在於確保資料的一致性和可追蹤的特性。上面這個類別繼承了DbContext,一旦它繼承了這個類別它就具有Code First的Context所有應有的能力。在上述程式碼中可以看到這個類別中有一個屬性:

      public DbSet<UserProfile> UserProfile {get; set;}

     可以將其解讀成這個資料庫中有一張資料表:UserProfiles。而建構式中傳遞給父類別建構子的字串其實我們可以在Web.config(註:不是Views資料夾中的Web.config)中找到:

 <connectionStrings>
    <add name="DefaultConnection" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=aspnet-MvcApplication1-20121118193713;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\aspnet-MvcApplication1-20121118193713.mdf" providerName="System.Data.SqlClient" />
  </connectionStrings>
 
   意思就是說明這個Context的資料庫連線字串為Web.config中所定義的。

    話題回到之前所討論的問題:其它類別是在做什麼的?
    其實,Model在系統開發上有好幾種,像是這種負責資料庫存取的Model我們會稱其為Data Access Object(資料存取物件),而為了讓View能夠輕鬆用來呈現畫面的就稱之為View Model(視圖物件),若僅是單傳用來在函式之間傳遞的則為Data Transfer Object(資料傳遞物件)。在AccountModels.cs檔案中的其它類別: RegisterExternalLoginModel, LocalPasswordModel, LoginModel, RegisterModel, ExternalLogin都屬於View Model。

總結:本章大略講解了AccountModels.cs這個檔案中的內容,但並非深入到Model的精髓,之後的文章會再深入到Model的開發技巧包括常用的設計樣式: 倉儲樣式, 單一工作樣式。

沒有留言:

張貼留言