Entity Framework Code First的POCO With DataAnnotation

  • 4262
  • 0

Entity Framework Code First的POCO With DataAnnotation

    Entity Framework主要是希望能夠達到一種"以習慣取代組態"的程式撰寫方法,因此,我們會在許多學習Entity Framework的文章或是書籍中發現到Entity Framework在組態檔(App.config或Web.config)方面著墨較少,反倒是常看到絕大部份的內容都是展現許多開發人員自定義好的POCO(Plain Old CLR Object)類別,並且在這個POCO類別的屬性上常會看到一些屬性(Atrribute)標籤,而這些屬性標籤正是邁向"以習慣取代組態"的一小步。

   首先,先定義好我們所需要的POCO類別,而本篇文章將以部落格系統作為基礎範例。

 

    步驟1. 設計檔案結構

       一個好的檔案結構能夠讓開發人員在找尋錯誤或是追蹤程式碼時有著較低的痛苦指數;雖然並不頻繁,但是偶爾我們仍是會需要去閱讀別人所撰寫的程式碼,若對方的程式碼並沒有一個有制度的檔案架構很容易會讓閱讀者感到吃力,是故,在開始動工之前應該要先討論好檔案結構以及命名規則等等程式規範。

      在本次範例中,由於是較小型的示範型專案,故沒有太大的商業價值且未來維護的需求幾乎等於0,在這種情況下可以將所有程式碼以資料夾拆分並且集中放在同一個專案即可;倘若未來這個專案會開始擴大且商業價值大幅提升時,就需要進行架構上的重構,將程式碼依團隊共識拆分到多個專案檔中。

Image

 

圖1. Model的檔案架構

      如上圖1所示,我們以Model資料夾作為整個系統架構:MVC中的Model層來看待,而其內部的程式依其職責和特性則是區分成三大類:

      ● Context : Code First中的上下文物件。

      ● POCO : POCO類別。

      ● Repository : 倉儲物件;將資料存取的功能封裝成一個物件。

      而在開發Model的流程中,通常會先撰寫POCO類別,接著再去撰寫Context,最後才是Repository物件。

 

   步驟2. 撰寫POCO類別


public class Destination
{
   public int DestinationId { get; set; }

   public string Name { get; set; }

   public string Country { get; set; }

   public string Description { get; set; }

   public byte[] Photo { get; set; }

   public virtual ICollection< Lodging> Lodgings { get; set; }
}

public class Lodging
{
   public int LodgingId { get; set; }

   public string Name { get; set; }

   public string Owner { get; set; }

   public bool IsResort { get; set; }

   public decimal MilesFromNearestAirport { get; set; }

   public Destination Destination { get; set; }
}

     在Entity Framework中會有開發人員指定的規則以及Entity Framework本身默認的規則。在開發系統時,資料庫設計一定免不了要特別去思考資料表與資料表之間的關係,而為了表達資料表與資料表之間的關係,通常會使用實體關聯圖來描述。

images

圖2. 實體關聯圖

在上圖2中描述著兩個實體-Student與Team之間的關係。設計POCO時當然亦可以參考實體關聯圖中描述的實體與實體之間的關係。畢竟物件導向的系統就是真實世界在某個上下文邊界下的映射。

      以Destination與Lodging這兩個POCO來說,兩者必定是任一個目的地都會有多個落腳處,故,兩者之間的關係必定是"一對多的關係"。在Entity Framework中,如果在一個POCO裡面撰寫其屬性的型別為ICollection且泛型型別為另一個POCO時,此時,Entity Framework會判定這兩個POCO必定是一對的關係。

 

      步驟3. 撰寫上下文類別

      一個上下文指的是在某一情境下參與的人事物,而這些人事物彼此有關係。因此,開發一個上下文類別應該要將心思放在情境上,要選擇在這個情境下有那些POCO參與,而不是一股腦的將所有POCO都放到一個上下文類別,令這個上下文類別成為一個超級的上下文類別,雖然這麼做在開發初期的確很方便,但是到了系統進入維護階段抑或是進入了需求變更風暴階段都會讓開發人員疲於奔命。

     由於本次範例的POCO數量較少且僅有一個情境,故僅需撰寫一個上下文類別即可。


public class BreakAwayContext: DbContext
{
   public DbSet< Destination> Desinations { get; set; }
   public DbSet< Lodging> Lodgings { get; set; }

   public BreakAwayContext()
      : base( "name=BreakAway" )
   {
      Database.SetInitializer( new CreateDatabaseIfNotExists <BreakAwayContext >());
   }
}

      在上下文類別中其實體的存取範圍都是public,並且型別都是DbSet且泛型型別為開發人員定義的POCO類別。除此之外,最重要的就是上下文類別必定要繼承自DbContext類別,如此,當系統啟動後且使用者操作到需要資料庫操作時,便會自動建立資料庫以及兩張資料表: Destination和Lodging。

      為達此目的,我們在HomeController中對Index這個Action撰寫如下冗餘程式碼:


public ActionResult Index()
{
   BreakAwayContext ctx = new BreakAwayContext ();
   ctx.Desinations.FirstOrDefault();
   return View();
}

      資料庫所建立的資料表如下圖所示:

 

Image

圖3. 資料庫所產生的資料表

從上圖3中可以發現到資料庫在建立資料時與我們所期望的有不小的差異,譬如說,針對字串型別的資料系統對於字串的長度應該要有一定的限制,不該讓所有字串型的資料都是允許無限大的,關於這一點較偏向是資料庫底層資料表設計的部份。雖然Entity Framework在POCO的設計上是較針對物件導向系統設計,但是我們仍不可忽視底層資料存取設計的不良會導致系統效能不佳的事實。在所謂的"Entity Framework默認規定"中,只要POCO中的屬性其型別為字串者,在建立資料表時一律將其設定為nvarchar(max)。因此,由這一點我們體認到,Entity Framework的默認規則並不一定適合所有系統的需求。

 

      Entity Framework的默認規則如下:

      1. 資料表/結構綱要的命名規則:

          Entity Framework對於英文命名的POCO,在建立資料庫的資料表時會將其轉成複數型命名;例如:若某一個POCO的名稱為Destination,則其資料表的名稱為:Destinations。

          所有由Entity Framework所建立的資料庫物件皆是隸屬於dbo這個綱要。

 

      2. 主鍵

          Entity Framework會將POCO中屬性名稱的結尾為: Id且不為Nullable的屬性視為主鍵;因此,在圖3中Destinations這張資料表的主鍵即為DetinationId。除此之外,由於此一屬性的資料型別為整數int,故Entity Framework除了將其設定為主鍵之外還會將其設定為識別欄位,當新增一筆資料時會自動累加1作為新資料的主鍵值。

 

      3. 字串

          只要是POCO的屬性其資料型別為字串者,皆設定其資料庫資料欄位型別為nvarchar(max),並且允許空值(Null)。

 

      4. Byte陣列

          只要是POCO的屬性其資料型別為Byte陣列者,皆設定其資料庫資料欄位型別為varbinary(max),並且允許空值(Null)。

 

      5. 布林

          只要是POCO的屬性其資料型別為布林者,皆設定其資料庫資料欄位為bit,並且不允許空值(Null)。

 

      6. 一對多關係

          在Destination與Lodging這兩個POCO的屬性中,可以看到Destination最後一個屬性的資料型別為IColleciont<Lodging>,而相對應的在Lodging中的最後一個屬性的資料型別亦為Destination。在圖3中Lodgings這張資料表的最後一個欄位:Destination_DestinationId,並且設定其參考到Destinations這張資料表的主鍵,此為Entity Framework在POCO的一對多設計上的一個默認規則。

           若僅是在Detination這個POCO中擁有ICollection<Lodging>型別的屬性,而在Lodging這個POCO沒有Destination型別的屬性,Entity Framework仍會判定此兩者的關係為一對多,其建立資料表時仍會如上圖3那般建立。

   由上述六點Entity Framework讓我們必須對於系統的設計做更進一步的修改,以期讓系統能有符合預期的品質要求。

 

   步驟4. 添加適當的Data Annotation

   經過了上面三個步驟之後,基本上系統的雛型已經具備,但是如步驟3所提到的Entity Framework的默認規則不適用於每個系統,故我們需要對POCO類別的設計進行再設計的動作,而這一次我們將更深入思考POCO類別中的每個屬性的限制。

      1. Key

          除了可以考慮使用Entity Framework的默認規則來設定某個POCO的屬性為主鍵之外,還可以使用Key這個資料標籤(Data Annotation)中的Key進行設定。


[Key]
public int DestinationId { get; set; } 

 

在Detination的屬性:DestinationId上加上[Key]即可設定其欄位為主鍵。

 

      2. Required

          在限制中,若在系統分析階段就已經瞭解到某些資料必須要存在不能為空值時,此一限制我們可以採用Entity Framework所提供的資料標籤(Data Annotation)中的Required進行設定。


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

 

      3. MaxLength/MinLength

          針對字串型別的屬性設定其字串長度要小於(MaxLength)或大於(MinLength)限制值。比較特別的是,這一組資料標籤亦會針對前端網頁進行輸入的檢查。


[Required]
[MaxLength(20)]
public string Name { get; set; }

          在Destination的屬性:Name上再加上[MaxLength]。每個POCO屬性上的資料標籤是可以累堆的。

 

      4. NotMapped

          在物件導向系統中,類別某些屬性是屬於計算型屬性;亦即它是計算或是綜整某些屬性的值。舉例來說,人的姓名會拆分成為兩個屬性:姓與名,但是若要取得完整的名字則需要將兩個屬性的值作額外的處理,若是有一個屬性可以直接取得,就不需要如此大費周張。但是這個屬性的值並不需要放入資料庫中,故在此一限制之下,我們要告訴Entity Framework要排除掉這個屬性。


[NotMapped]
public string Name { get { return FirstName+LastName;  } }

 

      5. ComplexType

          大多數的POCO其屬性型別都是以基礎資料型別為主,只有極少數會使用到自訂的型別。並且這也是POCO設計上要特別避免的事情,但是某些特殊需求時會需要使用到自訂型別但是該型別並非是作為一個系統的實體,而僅僅只是將這個類別當作一個型別來看待。較常會使用到的情境都是紀錄地址,因為地址如果單純以字串紀錄則會造成資料格式不統一或是曖昧,是故,需要有一個型別來輔助。


[ComplexType]
public class Adress
{
   [MaxLength(3)]
   public string District { get; set; }

   [MaxLength(20)]
   public string Street { get; set; }
 
   public int No { get; set; }

   public int Floor { get; set; }
}

          若POCO使用到自定義型別,則會在建立資料表時將其所有屬性皆以: 型別名稱_型別屬性 成為資料表的欄位。

Image

圖4. Lodgings套用Address自定型別

 

      6. ConcurrentcyCheck

          在多人連線線上系統中,難免會出現資料衝突的狀況,而資料衝突需要被偵測出來並且依照系統分析的協議來決定是該採用"先寫先贏"還是"後寫先贏"。Entity Framework提供一個資料標籤供開發人員針對POCO中某一個屬性進行資料衝突的偵測,當多個使用者修改到同一筆資料的該屬性,此時,系統會拋出"DbUpdateConcurrencyException"的異常。


[ConcurrencyCheck]
[Required]
[MaxLength(20)]
public string Name { get; set; }

           在程式撰寫上,針對具有ConcurrencyCheck的屬性,需要與修改其它屬性上較不同。


BreakAwayContext ctx = new BreakAwayContext ();
Destination dest = ctx.Desinations.FirstOrDefault();
ctx.Entry< Destination>(dest).State = System.Data.EntityState .Modified;
ctx.Entry< Destination>(dest).Property(p => p.Name).OriginalValue = "Any" ;
ctx.SaveChanges();

 

      7.  TimeStamp

          在某些特定情況下,若資料衝突偵測並不想要放在某一個屬性上,而是把焦點放在整筆資料列上,這個時候會額外增加一個屬性作為整筆資料的版本;這個屬性的型別需強制為Byte陣列。


[Timestamp]
public byte [] TimeStamp { get; set; }

Image

圖5. Destinations資料表套用資料版本欄位

 

      8. Table/Column

          某些情況下,物件導向的POCO物件其名稱希望與資料表不同,而Entity Framework提供Table這個資料標籤供開發人員自定自己的資料表命名。


[Table("SalesDestination")]
public class Destination

Entity Framework在資料型別上有著默認的規則,但是這些默認規則無法適用於所有需求;例如,某些字串型別的屬性僅會有數字或英文的資料,而Entity Framework卻默認所有字串型別對應到資料庫型別都是nvarchar(max),為了提示Entity Framework使用符合需求的型別,Entity Framework提供Column這個資料標籤供開發人員指定資料庫資料型別。


[Column(TypeName="char" )]
[MaxLength(50)]
public string EMail { get; set; }

 

      9. ForeignKey/InverseProperty

          在系統設計時,我們會發現到有時候會希望能夠在主從表單中,由子表單中的某筆資料直接取得與之相對應的主表單的該筆資料鍵值,這個時候在設計POCO類別會特別添加一個屬性,而該屬性即為紀錄與之關聯的POCO的鍵值。


public int DestinationId { get; set; }

[ForeignKey("DestinationId")]
public Destination Destination { get; set; }

          在實做上,某些特殊狀況下會希望能夠盡量簡化設計,特別是較為複雜的系統更是希望能夠保持POCO的精簡與純粹,當某個POCO與其它POCO是一對多關係,且擔任主表單的POCO其多個屬性會使用到擔任子表單的POCO,而在這種狀況下就是常見的"系統與資料庫阻抗不匹配";因為,在資料庫設計上這必需採用繞過(Bypass)的方式才能達到,這形成物件導向的系統規劃與資料庫設計有著差異,但這種情形卻是在系統設計上較常遇見的。Entity Framework提供一個資料標籤來達成上述的情境,並且保持POCO的精簡與純粹。

          在Lodging這個POCO加上兩個屬性:


public Destination FirstLevel { get; set; }

public Destination SecondLevel { get; set; }

          在Destination中加上兩個屬性:


[InverseProperty ("FirstLevel")]
public virtual ICollection< Lodging> FirstLevel_Lodgings { get; set; }

[InverseProperty ("SecondLevel")]
public virtual ICollection< Lodging> SecondLevel_Lodgings { get; set; }

Image

圖6. 加上InverseProperty後的結果

 

   步驟5. 最終修改結果


 [Table("SalesDestination")]
 public class Destination
 {
     [Key]
     public int DestinationId { get; set; }

     [Required]
     [MaxLength(20)]
     public string Name { get; set; }

     [Required]
     [MaxLength(20)]
     public string Country { get; set; }

     [MaxLength(255)]
     public string Description { get; set; }

     public byte[] Photo { get; set; }

     [Timestamp]
     public byte[] TimeStamp { get; set; }

     [InverseProperty ("FirstLevel")]
     public virtual ICollection< Lodging> FirstLevel_Lodgings { get; set; }

     [InverseProperty ("SecondLevel")]
     public virtual ICollection< Lodging> SecondLevel_Lodgings { get; set; }
 }

    [ComplexType]
    public class Address
    {
        [MaxLength(3)]
        public string District { get; set; }

        [MaxLength(20)]
        public string Street { get; set; }

        public int No { get; set; }

        public int Floor { get; set; }
    }

     public class Lodging
    {
        public int LodgingId { get; set; }

        [MaxLength(100)]
        public string Name { get; set; }

        [MaxLength(20)]
        public string Owner { get; set; }

        [Column(TypeName= "char")]
        [MaxLength(50)]
        public string EMail { get; set; }

        public bool IsResort { get; set; }

        public decimal MilesFromNearestAirport { get; set; }

        public Address Address { get; set; }

        public int DestinationId { get; set; }

        [ForeignKey("DestinationId")]
        public Destination Destination { get; set; }

        public Destination FirstLevel { get; set; }

        public Destination SecondLevel { get; set; }
    }