Code First :使用Entity. Framework編程(3)


第三章

對屬性使用約定和配置

在第2章,對Code First的約定以及如何通過配置覆寫默認約定行為進行了大致的介紹。學習了如何使用Data Annotations進行配置,也學習了如何使用Fluent API作出相同的配置,並對兩者進行了對比。

在本章乃至以后幾章里,將深入各種用於配置模型的領域。對每個主題會看到Code First如何通過默認規則進行工作,也會學到如何通過Data Annotations和Fluent API來覆寫這些規則。前已指出,在Fluent API中可以實現的很多配置在Data Annotations無法實現。我們會在適當的時機指出這些差異。

本章專注於對類中屬性的配置,以觀察默認規則和配置對數據庫列的影響。你將會學習到諸如如何控制字符串長度,byte 數組,數值的精度等方面的知識。你也可以學到鍵屬性以及所謂的“開放式並發屬性”。最后,您還可以學到有關Code First檢測一個屬性是否是復雜類型(aka值類型),如果Code First無法從您的域類中推斷出復雜類型時,我們將都會您如何對Code First提供幫助以識別復雜類型。

在Code First中使用屬性

在第2章里,您已經看到一些應用於字符串屬性的規則和配置選項,在進入新的選擇我們快速回顧一下。

Length

字長

Convention

默認規則

max (type specified by database)

max(類型由數據庫指定)

Data Annotation

MinLength(nn)

MaxLength(nn)

StringLength(nn) 

Fluent 

Entity<T>.Property(t=>t.PropertyName).HasMaxLength(nn) 

字長用於描述數組的長度。包括對字符串和byte數組。 

Code First的默認規則string 或者byte數組的長度應為最大。根據不同的數據庫類型確定在數據庫最終的類型。對SQL Server而言,string 會生成nvarchar(max),而byte數組會生成varbinary(max).

你可以覆寫默認長度來設置在數據庫中的實際字長。長度的最大值會在EF框架將更新數據存入數據庫之前進行驗證。如果使用Data Annotation來配置,還可以為數組配置MinLength(最小長度)特性。最小長度特性也會得到EF驗證API的驗證,但不會影響數據庫。

數據類型

Convention

默認規則

The default column data type is determined by the database provider you are using. For SQL Server some example default data types are:

默認的列數據類型由數據庫決定,對SQL Server而言如下:

String : nvarchar(max)

Integer:int

Byte Array:varbinary(max)

Boolen:bit

Data Annotation 

Column(TypeName="XXX")

Fluent 

Entity<T>.Property(t=>t.PropertyName).HasColumnType("XXX")

第2章,您已經看到了幾個如何映射.Net類型到數據庫數據類型的例子。Destination和Lodging類包含有整型,字符串,Byte數組,布爾型變量。Code First通知數據庫選擇合適的數據類型匹配每一列。由於使用的是SQL Server數據庫,因此分別映射到nvarchar(max), int, varbinary(max)和bit類型。

根據您選擇的配置當然也可以指定到基他類型。例如,將字符串映射到數據庫的int數據類型,運行時DbModelBuilder就會拋出一個錯誤告知映射非法,然后會給出如何進行糾正的細節指示。

 

可空性和必需項配置

Convention

默認規則

Key Properties : not null in database

鍵屬性:在數據庫中為非空

Reference Types (String, arrays): null in the database

引用類型(String,數組):在數據庫中可空

Value Types (all numeric types, DateTime, bool, char) : not null in database

值類型(所有數字類型,日期,布爾,字符):在數據庫為非空

Nullable<T> Value Types : null in database

Nullable<T>值類型(可空類型):在數據庫可空

Data Annotation 

Required

Fluent 

Entity<T>.Property(t=>t.PropertyName).IsRequired

默認規則約定確保非可空的.Net類型要映射到數據庫的非可空字段,除此以外,任何鍵屬性都只能映射到非可空數據庫字段。 

如果你使用.Net的泛型Nullable<T>指定一個值類型(如int)為可空,將會映射到數據庫的一個可空字段。

在第2章您已看到如何使用配置指定一個屬性為必須項。使用Data Annotation的Required標記和Fluent的IsRequired屬性都可強制Lodging.Name屬性為必須項。在保存數據到數據庫之前,EF運行時會對必須屬性進行驗證;如果屬性沒有賦值就會拋出一個異常。另一個效果是,數據庫相應字段為非空。

映射鍵

Convention

默認規則

Properties named Id

屬性名為Id

Properties named [TypeName] + Id

屬性名為[類型名]+Id

Data Annotation

Key

Fluent 

Entity<T>.HasKey(t=>t.PropertyName

EF框架要求每個實體都有一個鍵。這個鍵用於上下文以保持每個獨立對象的跟蹤。鍵是唯一的而且經常由數據庫生成。Code First默認規則作出了同樣的預設。

回憶一下由Destination和Lodging類生成的數據庫,DestinationId和LodgingId的整型字段都被標記為主鍵和非空字段。如果進一步觀察二者的列屬性,你會發現這些字段是自增長的標識字段,如圖3-1所示,這是默認規則將整型量作為主鍵來管理。

大多數情況下,數據庫中的主鍵不是int就是GUID類型,盡管任意類型都可以作為鍵屬性。數據庫中的主鍵會是多個表的組成字段,類似地,一個實體的鍵也是某個類中的多個屬性之一。在本節結束的時候,你會看到如何配置復合鍵。

Code First默認規則對不合規鍵屬性的響應

如果在我們的類中我們意指的鍵碰巧滿足Code First默認規則,那么一切順利。但是如果不滿足規則呢?

我們向模型添加一個新類,Trip,見代碼3-1.Trip類沒有任何滿足實體鍵默認規則的屬性,但我們的意圖是Identifier屬性應該作為鍵。

Example 3-1. The Trip class without an obvious key property

 public class Trip

{
    public Guid Identifier {  getset; }
    public DateTime StartDate {  getset; }
    public DateTime EndDate {  getset; }
    public  decimal CostUSD {  getset; }
}

伴隨這個新類,我們需要在BrakAwayContext中添加一個DbSet<Trip>數據集:

public DbSet<Trip> Trips {  getset; }

 我們再次運行程序,在嘗試從類中創建模型時DbModel Builder拋出一個異常:

在模型生成過程中檢測到一個或多個驗證錯誤: 
System.Data.Edm.EdmEntityType: : 
實體類型"Trip"還沒有定義key。請為這個實體類型定義Key. 

由於沒有找到期望的默認Key屬性(Id或TripId),Code First無法繼續創建模型。需要明確的是,類型(GUID)與這個問題無關。如前所述,您可以使用任何的原始類型作為鍵。

使用Data Annotations配置Key

Data Annotation標識一個鍵只需要簡單的一個Key.Key特性位於System.ComponentModel.DataAnnotations.dll,由於它已經被添加到了.Net4之中,也被其他API所使用(如ASP.Net MVC使用的Key).如果你的項目尚未包含此程序集的引用,你就不能添加它。對這個特定的特性不需要引用EntityFramwork.dll。

 

[Key]
public Guid Identifier {  getset; }

 

在Flurent API中使用HasKey來配置Key屬性

使用Fluent API來配置Key屬性與前面幾個Fluent配置不同。這一配置直接添加到實體上。為了配置一個key,你需要使用HasKey方法,如代碼3-2。

Example 3-2. The HasKey Fluent configuration in OnModelCreating

 

modelBuilder.Entity<Trip>().HasKey(t => t.Identifier)

 

如果將代碼配置進EntityTypeConfiguration類中,正如你在第2章學到的,應該開始於HasKey或This.HasKey(代碼3-3)

Example 3-3. HasKey inside of an EntityTypeConfiguration class

 

HasKey(t => t.Identifier)

 

配置數據庫生成的屬性

Convention

默認規則

Integer keys:Identity

整型鍵值:標識列

Data Annotation

DatabaseGenerated(DatabaseGeneratedOption)

Fluent 

Entity<T>.Property(t=>t.PropertyName)

.HasDatabaseGeneratedOption(DatabaseGeneratedOption)

在前面部分里,你已經看到默認情況整型鍵值會被EF框架生成標識字段,由數據庫生成值。而我們自己創建的Guid型的鍵值怎么辦?Guid需要特殊的處置,包含在DatabaseGenerated配置。

為了展示,我們添加一個新方法,InsertTrip(代碼3-4)到控制台程序,然后在主模型進行調用。

Example 3-4. The InsertTrip method

 

復制代碼
private  static  void InsertTrip()
{
    var trip =  new Trip
   {
     CostUSD =  800,
     StartDate =  new DateTime( 201191),
     EndDate =  new DateTime( 2011914)
   };
    using ( var context =  new BreakAwayContext())
    {
      context.Trips.Add(trip);
      context.SaveChanges();
    }
}
復制代碼

 

運行程序會導致數據庫卸載並增加新的Trips表后重新創建,如圖3-2.Identifier是主鍵,唯一標識,非空列。

回到本章前面的內容,你知道值類型默認是required。在此也會看到同樣的效果,StartDate,EndDtat和CostUSD屬性都是值類型,默認情況下,在數據庫也都是非空字段。 

然后在新行中我們看到Guid值被填充為很多個0.如圖3-3

數據庫和EF框架都不知道我們想讓他們之一為新添加的Trips生成一個新的Guid。由於這個屬性沒有一個生成新Guid的邏輯方法,就會默認以0值填入。

如果你嘗試以同樣的值插入另一個記錄,數據庫會拋出一個錯誤,因為期待一個唯一值。當然可以配置數據庫自動生成一個新的Guid(通過設置默認值為newid()。不管你在數據庫中手動操作還希望CodeFirst插入此邏輯,你必須讓Code First知道數據庫將要處理Guid.

解決方案是讓Code First 知道數據庫將要生成這個鍵值通過使用另一個annotation:DatabaseGenerated.這一配置有三個選項—None,Identity和Computed.我們想要Identifier字段被標識為Identity,才能確保數據庫在加入新行時自動生成標識字段的值,正如整型類型的鍵值自動生成一樣。

使用Data Annotations配置數據庫-生成選項

修改類代碼告訴Code First讓數據庫生成一個唯一的鍵值:

 

[Key,DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Identifier {  getset; } 

當鍵字段為整數時,Code First默認選擇DatabaseGeneratedOption.Identity。而對Guid,你需要顯示進行配置。這是唯一一種可以通過Identify來配置Code First的數據類型。如果映射到一現有的數據庫,任何在插入數據可生成值的列都可以標識為Identify.

再次運行程序,如圖3-4,輸出了新生成的標識。

你可能對查看SQL語句感興趣,代碼3-5顯示的EF框架發送給數據庫的INSERT語句,其中要求數據庫為Idenfifier屬性生成Guid值

Example 3-5. SQL for inserting a new Trip

 

復制代碼
declare  @generated_keys  table( [ Identifier ]  uniqueidentifier)
insert  [ dbo ]. [ Trips ]( [ StartDate ][ EndDate ][ CostUSD ])
output inserted. [ Identifier ]  into  @generated_keys
values ( @0@1@2)
select t. [ Identifier ]
from  @generated_keys  as g
join  [ dbo ]. [ Trips ]  as t  on g. [ Identifier ]  = t. [ Identifier ]
where  @@ROWCOUNT  >  0 ' ,
N
' @0 datetime2( 7), @1 datetime2( 7), @2  decimal( 18, 2) ' ,
@0=
' 2011 - 09 - 01  00: 00: 00 ' ,@1= ' 2011 - 09 - 14  00: 00: 00 ' ,@2=800.00
 
復制代碼

 

DatabaseGeneratedOption還有兩個枚舉值:None和Coumputed。下面就有一個示例證明None是有用的。代碼3-6顯示了另一個新類,Person,SocialSecurityNumber屬性已經被配置為此類的鍵屬性。

 

Example 3-6. Person class with unconventional key property

 

復制代碼
using System.ComponentModel.DataAnnotations;
namespace Model
{
    public class Person
  {
     [ Key ]
     public  int SocialSecurityNumber { get;  set; }
     public string FirstName { get;  set; }
     public string LastName { get;  set; }
  }
}
復制代碼

 

記得要在BreakAwayContext類中添加DbSet<Person>

public DbSet<Person> People { get; set; } 

最后,將一個新方法,InsertPerson(見代碼3-7)添加到控制台程序中,在Main方法中調用這個方法,就會向數據庫中添加一個新的person。

Example 3-7. InsertPerson method

 

復制代碼
private static void InsertPerson()
{
    var person  = new Person
  {
     FirstName  = "Rowan",
     LastName  = "Miller",
     SocialSecurityNumber  =  12345678
  };
   using ( var context  = new BreakAwayContext())
  {
     context.People. Add(person);
     context.SaveChanges();
  }
}
復制代碼

 

再次運行程序,讓我們再看看數據庫新添加的一行,如圖3-5.

SocialSecurityNumber 的值是 1, 不是12345678.為什么?由於Code First根據key是一個整型這個事實告知數據庫這是一個標識字段,因此在INSERT語句中EF框架沒有提供正確的SocialSecurityNumber 的值,而是讓數據庫自行生成。從而在SaveChanges完成后查看 person實例中SocialSecurityNumber 的值,此值已經被更新為數據庫生成的值,1.

為修正這一點,我們需要添加一些配置覆寫默認標識規則,在這種情況下,DatabaseGeneratedOption.Identity是不對的,應該用None:

 

[ Key, DatabaseGenerated(DatabaseGeneratedOption.None) ]
public  int SocialSecurityNumber { get;  set; } 

 然后再運行程序,如圖3-6,數據庫正確插入了有關數據。

DatabaseGeneratedOption.Computed用於指定一個映射到數據庫的字段是通過計算得到的。例如,如果有一個FullName字段在People表中,是用一個公式將FirstName和LastName組合起來得到的,你就應該讓EF框架知道以便其不會嘗試存儲數據到此列中。你不能指定一個公式用來計算Code First中列的值,因此當映射到一個現存的數據庫中你只能使用Computed。要不然,在試圖創建數據庫時如果遇到Computed配置,數據庫引擎就會拋出運行時異常。

使用Fluent API來配置數據庫生成選項

DatabaseGeneratedOption可以配置為一種特殊的屬性,你可以將配置附加HasKey后面,如:

 

modelBuilder.Entity <Trip >()
.HasKey(t  => t.Identifier)
.Property(t  => t.Identifier)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption. Identity); 

 

或者創建一個獨立的語句:

 

modelBuilder.Entity <Person >()
.HasKey(p  => t.SocialSecurityNumber);
modelBuilder.Entity <Person >()
.Property(p  => p.SocialSecurityNumber)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.None); 

 

你會注意到DatabaseGeneratedOption枚舉位於System.ComponentModel.DataAnnotations名稱空間,在EntityFramework.dll中。需要在context類的文件頭部添加using引用。

為開放式並發環境配置時間戳或行版本字段

Convention

默認規則

None

Data Annotation

TimeStamp

Fluent 

Entity<T>.Property(t=>t.PropertyName).IsRowVersion()

EF框架從第一版本開始就支持開放式並發環境。Programming Entity Framework這本書的第二版在第23章深入探討了開放式並發。在這里我我們教你如何配置類映射到RowVersion(或稱作TimeStamp,時間戳)字段,同時通知EF框架在進行更新或刪除數據庫操作時使用這些字段進行並發檢查。

使用Code First你需要指定一個字段使用開放式並發檢查,與映射到數據庫的類型無關,或者你可以進一步指定並發的字段映射到一個TimeStamp字段。

一個類只能有一個屬性可以配置為TimeStamp特性。

RRowVersion和TimeStamp是兩個具有相同類型的項。Sql Server使用TimeStamp,而其他數據庫使用更恰當的名稱為RowVersion.d SQL Server2008中,timestamp數據類型也調整為rowversion,但是大多數工具(如Sql Server Management Studio,vs等)仍然顯示為timestamp.

Code First的默認規則與TimeStamp字段

默認情況下,Code First並不識別時間戳屬性,因此沒有默認約定行為,獲得此行為必須配置此屬性。

使用Data Annotations配置時間戳

並非任何屬性都可以映射到一個timestamp數據庫類型。必須是byte數組才可以。配置過程很簡單,將TimeStamp特性加到Trip和Personal類中的下列屬性中。

 

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

 

 然后運行控制台程序,確保InserTrip和InsertPerson方法都在Main方法中進行調用。在數據庫中你會看到新生成的RowVersion列(圖3-7),類型為非可空timestamp類型。

任何時候行內數據被修改時數據庫都會自動為此屬性創建新值。但TimeStamp不僅影響數據庫的映射,還會導致屬性被EF框架視作並發的令牌。如果你使用EDMX文件,這就等同於設置了一個屬性的ConcurrencyMode(並發模式)。EF框架在執行插入、更新或刪除數據庫時,就會考慮並發字段,返回每個INSERT和UPDATE更新數據庫的值,並傳回到每個UPDATE和DELETE的相關屬性的原始位置。

例3-8顯示了當執行InsertPerson方法后保存設置時的SQL語句:

Example 3-8. INSERT combined with SELECT to return new RowVersion

exec sp_executesql N
' insert [dbo].[People]([SocialSecurityNumber], [FirstName], [LastName])
values (@0, @1, @2)
select [RowVersion]
from [dbo].[People]
where @@ROWCOUNT > 0 and [SocialSecurityNumber] = @0
',

N'@0 int,@1 nvarchar(max) ,@2 nvarchar(max) ',@0=12345678,@1=N'Rowan',@2=N'Miller' 

EF框架不僅通知數據庫執行插入,而且還請求返回RowVersion的值。一旦屬性被標記為並發,EF就總會這樣做,即使它並不是一個timestamp類型數據。對更新和刪除語句更是如此,因為在這會有並發檢查產生。我們添加一個新的方法,UpdatePerson到程序中,見代碼3-9

Example 3-9. The UpdateTrip method

復制代碼
private  static  void UpdateTrip()
{
    using ( var context =  new BreakAwayContext())
  {
     var trip = context.Trips.FirstOrDefault();
    trip.CostUSD =  750;
    context.SaveChanges();
  }
復制代碼

代碼3-10顯示了當調用UpdatePerson時的SQL語句:

Example 3-10. UPDATE that filters on original RowVersion and returns new RowVersion

 

復制代碼
exec sp_executesql N ' update [dbo].[Trips]
set [CostUSD] = @0
where (([Identifier] = @1) and ([RowVersion] = @2))
select [RowVersion]
from [dbo].[Trips]
where @@ROWCOUNT > 0 and [Identifier] = @1
',
N ' @0 decimal(18,2),@1 uniqueidentifier,@2 binary(8) ',
@0 = 750.00, @1 = ' D1086EFE-5C5B-405D-9F09-688981BB5B41 ', @2 = 0x0000000000001773 
復制代碼

 

注意謂詞Where用於定位trip的語句被更新—過濾器包括了Identifier和Rowversion兩個參數。如果另外的人更改了行程就會被我們的方法檢索到,由於RowVersion已經更改,將不會再有行匹配過濾器。更新就會失敗,EF框架會拋出OptimisticConcurrencyException的異常。

使用Fluent API配置TimeStamp/RowVersion

Fluent 使用RowVersion來配置,要指定一個RowVersion屬性,需要將IsRowVersion()方法附加到屬性上。

使用DbModelBuilder,需要對屬性作如下配置:

 

modelBuilder.Entity<Person>()
.Property(p => p.RowVersion).IsRowVersion();
 在EnityTypeConfiguration<T>類中配置如下:

 

 

Property(p=>p.RowVersion).IsRowVersion();

 

配置並發非時間戳字段

Convention

默認規則

None

Data Annotation

ConcurrencyCheck

Fluent 

Entity<T>.Property(t=>t.PropertyName).IsConcurrencyToken()

一個不太常見的方式是並發檢查是通過字段為非行版本類型進行的。例如,許多數據庫可能並沒有行版本數據類型。因此你不能指定一個行版本屬性,但你仍需要對一個或多個數據庫字段進行並發檢查。

Person類當前使用屬性SocialSecurityNumber作為其標識鍵。設想類使用了PersionId屬性作為標識鍵而將SocialSecurityNumber簡單地視作整型數據而不作為標識跟蹤。在這種情況下,你可能想有一種方法避免在SocialSecurityNum ber進行改變時的沖突,因為在美國,每個公民的社會保險號碼是唯一的。因此,如果一個一個用戶編輯了一個人的記錄,可能更改的FirstName的拼寫,但同時,另外的人想更改此人的社會保險號碼,前者在嘗試存儲更改時就會遇到一個沖突。指定SocialSecurityNumber屬性為一個並發檢查字段將提供這種檢查(避免這種事情發生)。

使用Data Annotations配置開放式並發

代碼3-11顯示了修改的類為SocialSecurityNumber配置並發檢查

Example 3-11. Modified Person class with a ConcurrencyCheck

 

復制代碼
public  class Person
 {
    public  int PersonId {  getset; }
   [ConcurrencyCheck]
    public  int SocialSecurityNumber {  getset; }
    public  string FirstName {  getset; }
    public  string LastName {  getset; }
}
復制代碼

 

例3-12顯示了一個方法試圖更新一個Person.如果調用這個方法,就需要先調用InsertPerson以確保數據庫內存在一個Person數據。

Example 3-12. The UpdatePerson method

 

復制代碼
private  static  void UpdatePerson()
{
    using ( var context =  new BreakAwayContext())
  {
     var person = context.People.FirstOrDefault();
    person.FirstName =  " Rowena ";
    context.SaveChanges();
  }
}
復制代碼

 

正如您在Trip.RowVersion字段中看到的(代碼3-10),當一個更新或刪除請求發送以數據庫時,SLQ語句(見代碼3-13)不僅查找匹配的Key(PersonId),還要匹配原始並發字段值(SocialSecurityNumber).

Example 3-13. SQL providing concurrency checking on SocialSecurityNumber

 

exec sp_executesql N ' update [dbo].[People]
set [FirstName] = @0
where (([PersonId] = @1) and ([SocialSecurityNumber] = @2))
',N ' @0 nvarchar(max) ,@1 int,@2 int ', @0 =N ' Rowena ', @1 = 1, @2 = 12345678

 

如果匹配沒有發現(也就是說SocialSecurityNumber已經在數據庫中變更了),更新失敗拋出OptimisticConcurrencyException異常。

使用Fluent API的開放式並發配置

Fluent API使用IsConcurrencyToken方法配置並發,並應用於屬性。如代碼3-14所示

Example 3-14. Configuring concurrency checking fluently

 

復制代碼
public class PersonConfiguration : EntityTypeConfiguration <Person >
{
      public PersonConfiguration()
  {
     Property(p  => p.SocialSecurityNumber).IsConcurrencyToken();
  }
}
復制代碼

 

我們為Person提供其自己的配置類,也就是這個新類。不要忘記在OnModelCreating方法中將PersonConfiguration添加到modelBuilder.Configurations集合里。

映射到非-Unicode數據庫類型

Convention

默認規則

All strings map to Unicode-encoded database types

所有的字符串都映射到Unicode數據庫類型

Data Annotation

不可用

Fluent 

Entity<T>.Property(t=>t.PropertyName).IsUnicode(boolean)

默認情況下,Code First會將所有字符串都映射到數據庫中的Unicode字符串類型。 

你可以使用IsUnicod方法指定一個字符串是否映射到數據庫Unicode字符串類型。下列代碼添加到LodgingConfiguation中告訴Code First不要將Owner屬性作為Unicode 類型:

Property(l=>l.Owner).IsUnicode(false);

對Decimal固定有效位數和小數位數的影響

Convention

默認規則

Decimals are 18, 2

 

Data Annotation

不可用

Fluent 

Entity<T>.Property(t=>t.PropertyName).HasPrecision(n,n)

固定有效位數(一個數字中數的位數)和小數位數(小數點右側的位數)可以使用Fluent API進行配置,而不能用Data Annotations配置。

為了觀察其如何工作,我們向Lodging 類中添加一個新的decmial屬性:MilesFromNearestAirport:

 

public  decimal MilesFromNearestAirport { get;  set; }

 

默認設置

默認情況下,固定有效位數為18,小數位為2,如圖3-8所示。

使用Flurent API,可以對固定有效位和小數位進行配置,使用的是HasPrecison方法。即使默認值之一是想要設置的的,也需要將兩個值都指定:

 

Property(l  => l.MilesFromNearestAirport).HasPrecision( 81);

 圖3-9顯示了MilesFromNearestAirport有效位和小數位的更改情況。

在Code First使用復雜類型

EF框架從第一版開始就支持復雜類型。復雜類型也可視作值類型(?)可以作為附加屬性添加到其他類。復雜類型與實體類型的區別在於復雜類型沒有其自己的鍵。它是依賴於其"宿主"類型跟蹤變化 和持久化。

一個沒有Key屬性的類型,並且作為屬性映射到一個或多個類型中,Code First就會將其視作為復雜類型。Code First將預設復雜類型的屬性出現在宿主類型映射到數據庫的表中。

在People表中如何將Person中的Address包含進來,將Address的屬性都映射到People表中?可以直接將所有相關屬性都納入Person類中,見代碼3-15:

Example 3-15. Individual properties representing an address in Person

 

復制代碼
    public class Person
   {
     public  int PersonId { get;  set; }
     public  int SocialSecurityNumber { get;  set; }
     public string FirstName { get;  set; }
     public string LastName { get;  set; }
     public string StreetAddress { get;  set; }
     public string City { get;  set; }
     public string State { get;  set; }
     public string ZipCode { get;  set; }
    }
復制代碼

 

但在你的模型中如果使用Address類作為分割類,就可以簡化Person類,如代碼3-16所示:

Example 3-16. Address type as a property of Person

復制代碼
public class Address
{
public  int AddressId { get;  set; }
public string StreetAddress { get;  set; }
public string City { get;  set; }
public string State { get;  set; }
public string ZipCode { get;  set; }
}
public class Person
{
public  int PersonId { get;  set; }
public  int SocialSecurityNumber { get;  set; }
public string FirstName { get;  set; }
public string LastName { get;  set; }
public Address Address { get;  set; }
復制代碼

}

但是如果這樣分割,使用默認規則,會產生一個單獨的表:Addresses。而我們目標是讓People表中擁有一系列地址字段。如果Adress是一個復雜類型就可以達到這個目的。如果你有其他表中也包含相同的屬性,你也可以在那些類中使用Address復雜類型。

定義默認復雜類型

最方便的方法將Address轉化為復雜類型是移除AddressId 屬性。現在注釋掉它:

// public int AddressId { get; set; }

在重新運行程序之前,你需要考慮InsertPerson方法(代碼3-7)之前 Address是否存在。因為Address屬性沒有處理將會成為null值,將會造成SaveChanges拋出DbUpdateException異常。現在可以向代碼中插入一個新的Person,並且在Person類中實例化一個新的Address。

Example 3-17. Instantiating the Address property in the constructor of the Person class

 

復制代碼
public  class Person
{
public Person()
{
Address =  new Address();
}
//
}
復制代碼

 

除了復雜類型不能有Key以外,Code First中還有兩條規則用於檢測復雜類型是否滿足要求。復雜類型在應用於其他類時只能包含原始屬性,只能被用作為非集合類型。換句話說,如果想要Person類中有一個List<Address>或其他Address類型的集合類型屬性,Address不能作為復雜類型。

復雜類型的默認規則 
  1. 復雜類型無Key屬性 
  2. 復雜類型只包含原始屬性 
  3. 用作其他類的屬性時,屬性必須是一個單一實例,不能用於集合類型 

運行程序后,圖3-10顯示了Address字段成為了People表的一部分。Code First認定Address是一個復雜類型,在新模型生成得到響應:

注意Address 字段的命名:HostPropertyName_Property。這是Code First 的默認設置。第5章,你就會學到如何為復雜屬性配置列名。 

配置非默認復雜類型

如果要使用復雜類型,必須要遵循這些規則嗎?可能您想要有一個AddressId屬性,盡管你知道一個單獨的地址實例不會變更,也不需要EF框架對其跟蹤。

如果我們添加AddressId屬性重新運行程序,Code First不能推斷出你的意圖,然后會創建一個單獨的Addersses表,並建立與People表的主外鍵關系。你可以顯示地配置復雜類型來修正。

使用Data Annotations指定復雜類型

Data Annotation提供了ComplexType特性應用於類上。

Example 3-18. Address with AddressId reinstated and a ComplexType configuration

 

復制代碼
[ComplexType]
public  class Address
{
public  int AddressId {  getset; }
public  string StreetAddress {  getset; }
public  string City {  getset; }
public  string State {  getset; }
public  string ZipCode {  getset; }
}
復制代碼

 

使用這種方式,再次運行程序,模型將重建,最終的數據庫架構再一次同圖3-10一致,另外附加了一個新的int字段,命名為Address_AddressId.

使用Fluent API指定復雜類型

為了通過Fluent API向Code First指明一個類型為復雜類型,你必須使用DbModelBuilder.ComplexType 方法。

 

modelBuilder.ComplexType<Address>();

 

代碼3-19顯示了對OnModelCreating方法的修改:

Example 3-19. Specifying a complex type fluently

 

復制代碼
protected  override  void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add( new DestinationConfiguration());
modelBuilder.Configurations.Add( new LodgingConfiguration());
modelBuilder.Configurations.Add( new PersonConfiguration());
modelBuilder.Configurations.Add( new TripConfiguration());
modelBuilder.ComplexType<Address>();
}
復制代碼

 

本modelBuilder的配置故意將新增加的配置代碼放在后面。那些直接通過內建在OmodelCreating方法內部建立的modelBuilder類的實例,稱為內聯配置,必須將這些代碼寫在添加的配置類集合的后面。

處理更多的雜亂的復雜類型

回想默認配置的復雜類型規定類型只能包含原始類型。如果你的復雜類型不符合這一規范,必須進行配置,這里有一些例子。

我們創建了兩個新類,PersonalInfo和Measuremet,見代碼3-20. PersonalInfo包含有兩個Measurement屬性。注意到在兩個類都沒有標識屬性。我們的意圖是兩個類都成為復雜類型。PersonalInfo復雜類型使用Measurment復雜類型,這就是所謂的嵌套復雜類型。

Example 3-20. New classes: PersonalInfo and Measurement

 

復制代碼
public  class PersonalInfo
{
public Measurement Weight {  getset; }
public Measurement Height {  getset; }
public  string DietryRestrictions {  getset; }
}
public  class Measurement
{
public  decimal Reading {  getset; }
public  string Units {  getset; }
}
復制代碼

 

當我們向Person類中添加新的PersonInfo屬性后:

 

public PersonalInfo Info {  getset; }

 

 也需要添加一些邏輯到Person的構造器,用具體實例說明這些屬性:

復制代碼
public Person()
{
Address =  new Address();
   Info =  new PersonalInfo
  {
    Weight =  new Measurement(),
    Height =  new Measurement()
  };

復制代碼

如果此時繼續運行程序,Model builder會拋出異常:

實體類"PersonInfo"沒有定義鍵。請為此實體類定義鍵。

Code First 並沒有將PersonalInfo識雖為復雜類型。原因是我們打破了規則:復雜類型必須只包含原生類型。在PersonalInfo類中有兩個Measurement類型的屬性。由於這是非原生類型,規則不能將PersonalInfo作為復雜類型。

如果添加ComplexType配置到PersonalInfo類,Code First就能夠將屬性建立到模型中。你不必配置Measurement類,因為它遵循復雜類的規則。

配置復雜類型的屬性

Code First將復雜類型屬性與其他類型以相同的方式處理,你可以用Data Annotations或Fluently來配置。

使用Data Annotations來配置復雜類型

與Code First默認命名列類似:ComplexTypeName_PropertyName(見圖3-10)。你可以應用Data Annotations在復雜類型上,正如你在其他類上使用的那樣。例3-21使用了一個你熟悉的特性標記,MaxLength,去影響 Address類型的中的屬性。

Example 3-21. Configuring the StreetAddress property of the Address

 

復制代碼
[ComplexType]
public  class Address
{
public  int AddressId {  getset; }
[MaxLength( 150)]
public  string StreetAddress {  getset; }
public  string City {  getset; }
public  string State {  getset; }
public  string ZipCode {  getset; }
}
復制代碼

 

Figure 3-11 shows the People table of the database with the modified Address_StreetAddress field. You can also see the Address_AddressId field that came from reinstating AddressId and the fields added as a result of the PersonalInfo complex type and its Measurement subtype.

圖3-11顯示了數據庫的People表,其Address_streetAddress字段已經被修改.你也可以看到Address_AddressId字段來自於AddressId,作為結果,新添加的了PersonalInfo復雜類型和它的Measurement子類型.

第5章,我們重新檢視復雜類型的列名,然后你會學習如何通過配置調整列名. 

使用Fluent API來配置復雜類型

使用Fluent API有兩種方法配置復雜類型屬性.你可以開始於宿主實體也可以開始於復雜類型本身.根據EF構架團隊的報告,后者是更好的配置方式.MaxLength屬於這一類.當我們在第5章討論列名時,我們會從Person實體中進行配置,以期對Person映射到字段的名字產生影響. 

Model builder能夠識別復雜類和實體類的差異.

如果你想直接從DbModelBuilder配置,必須開始於Compex<T>方法,而不是一直在用的Entity<T>方法.代碼3-22展示了直接在OnCreatingModel中的ModelBuilder實例配置復雜類型.

Example 3-22. Configuring a property of the Address complex type

modelBuilder.ComplexType<Address>()

.Property(p => p.StreetAddress).HasMaxLength(150);  

如果你更喜歡封裝配置,你需要繼承自ComplexTypeConfiguation類而不是EntityTypeConfigration,如代碼3-23.

Example 3-23. Configuring the length of StreetAddress in the Address ComplexType

 

復制代碼
public  class AddressConfiguration :
ComplexTypeConfiguration<Address>
{
public AddressConfiguration()
{
Property(a => a.StreetAddress).HasMaxLength( 150);
}
}
復制代碼

 

你還要確保在模型中添加AddressConfiguration:

 

modelBuilder.Configurations.Add( new AddressConfiguration());

 

小結

I在本章中你已經看到很多Code First創建的預設模型,這些模型都是基於你自己的類創建的.Strings在SQL Server中變成nvarchar(max).數字的固定位數設置為18位(可以確保你可以跟蹤1024的數據)和2位小數.這些及其他默認值在很廣泛的場合中很有關,你也可以根據需要通過應用配置指定位數.你已經學到如何確保EF框架知道如何將值設定為timestamps或至少是一個並發字段.你已經開始接觸到復雜類型,這些類型沒有鍵,只有它們是其他類的屬性時才能用到.

Code First的約定在大量通用場景都發揮了很好的作用,但通過你的配置來覆寫這些約定還可以控制如何由EF框架來管理你的類.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM