第三章
對屬性使用約定和配置
在第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 { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public decimal CostUSD { get; set; }
}
伴隨這個新類,我們需要在BrakAwayContext中添加一個DbSet<Trip>數據集:
我們再次運行程序,在嘗試從類中創建模型時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。
public Guid Identifier { get; set; }
在Flurent API中使用HasKey來配置Key屬性
使用Fluent API來配置Key屬性與前面幾個Fluent配置不同。這一配置直接添加到實體上。為了配置一個key,你需要使用HasKey方法,如代碼3-2。
Example 3-2. The HasKey Fluent configuration in OnModelCreating
如果將代碼配置進EntityTypeConfiguration類中,正如你在第2章學到的,應該開始於HasKey或This.HasKey(代碼3-3)
Example 3-3. HasKey inside of an EntityTypeConfiguration class
配置數據庫生成的屬性
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
{
var trip = new Trip
{
CostUSD = 800,
StartDate = new DateTime( 2011, 9, 1),
EndDate = new DateTime( 2011, 9, 14)
};
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讓數據庫生成一個唯一的鍵值:
public Guid Identifier { get; set; }
當鍵字段為整數時,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
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
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
{
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:
public int SocialSecurityNumber { get; set; }
然后再運行程序,如圖3-6,數據庫正確插入了有關數據。
DatabaseGeneratedOption.Computed用於指定一個映射到數據庫的字段是通過計算得到的。例如,如果有一個FullName字段在People表中,是用一個公式將FirstName和LastName組合起來得到的,你就應該讓EF框架知道以便其不會嘗試存儲數據到此列中。你不能指定一個公式用來計算Code First中列的值,因此當映射到一個現存的數據庫中你只能使用Computed。要不然,在試圖創建數據庫時如果遇到Computed配置,數據庫引擎就會拋出運行時異常。
使用Fluent API來配置數據庫生成選項
DatabaseGeneratedOption可以配置為一種特殊的屬性,你可以將配置附加HasKey后面,如:
.HasKey(t => t.Identifier)
.Property(t => t.Identifier)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption. Identity);
或者創建一個獨立的語句:
.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特性。
R
RowVersion和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類中的下列屬性中。
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
' 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
{
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
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,需要對屬性作如下配置:
.Property(p => p.RowVersion).IsRowVersion();
在EnityTypeConfiguration<T>類中配置如下:
配置並發非時間戳字段
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 int PersonId { get; set; }
[ConcurrencyCheck]
public int SocialSecurityNumber { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
例3-12顯示了一個方法試圖更新一個Person.如果調用這個方法,就需要先調用InsertPerson以確保數據庫內存在一個Person數據。
Example 3-12. The UpdatePerson method
{
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
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 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:
默認設置
默認情況下,固定有效位數為18,小數位為2,如圖3-8所示。
使用Flurent API,可以對固定有效位和小數位進行配置,使用的是HasPrecison方法。即使默認值之一是想要設置的的,也需要將兩個值都指定:
圖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 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 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 Person()
{
Address = new Address();
}
// …
}
除了復雜類型不能有Key以外,Code First中還有兩條規則用於檢測復雜類型是否滿足要求。復雜類型在應用於其他類時只能包含原始屬性,只能被用作為非集合類型。換句話說,如果想要Person類中有一個List<Address>或其他Address類型的集合類型屬性,Address不能作為復雜類型。
復雜類型的默認規則
-
復雜類型無Key屬性
-
復雜類型只包含原始屬性
-
用作其他類的屬性時,屬性必須是一個單一實例,不能用於集合類型
運行程序后,圖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
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; }
}
使用這種方式,再次運行程序,模型將重建,最終的數據庫架構再一次同圖3-10一致,另外附加了一個新的int字段,命名為Address_AddressId.
使用Fluent API指定復雜類型
為了通過Fluent API向Code First指明一個類型為復雜類型,你必須使用DbModelBuilder.ComplexType 方法。
代碼3-19顯示了對OnModelCreating方法的修改:
Example 3-19. Specifying a complex type fluently
{
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 Measurement Weight { get; set; }
public Measurement Height { get; set; }
public string DietryRestrictions { get; set; }
}
public class Measurement
{
public decimal Reading { get; set; }
public string Units { get; set; }
}
當我們向Person類中添加新的PersonInfo屬性后:
也需要添加一些邏輯到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
public class Address
{
public int AddressId { get; set; }
[MaxLength( 150)]
public string StreetAddress { get; set; }
public string City { get; set; }
public string State { get; set; }
public string ZipCode { get; set; }
}
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
.Property(p => p.StreetAddress).HasMaxLength(150);
如果你更喜歡封裝配置,你需要繼承自ComplexTypeConfiguation類而不是EntityTypeConfigration,如代碼3-23.
Example 3-23. Configuring the length of StreetAddress in the Address ComplexType
ComplexTypeConfiguration<Address>
{
public AddressConfiguration()
{
Property(a => a.StreetAddress).HasMaxLength( 150);
}
}
你還要確保在模型中添加AddressConfiguration:
小結
I在本章中你已經看到很多Code First創建的預設模型,這些模型都是基於你自己的類創建的.Strings在SQL Server中變成nvarchar(max).數字的固定位數設置為18位(可以確保你可以跟蹤1024的數據)和2位小數.這些及其他默認值在很廣泛的場合中很有關,你也可以根據需要通過應用配置指定位數.你已經學到如何確保EF框架知道如何將值設定為timestamps或至少是一個並發字段.你已經開始接觸到復雜類型,這些類型沒有鍵,只有它們是其他類的屬性時才能用到.
Code First的約定在大量通用場景都發揮了很好的作用,但通過你的配置來覆寫這些約定還可以控制如何由EF框架來管理你的類.