【译著】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 { 
get; 
set; }

public DateTime StartDate { 
get; 
set; }

public DateTime EndDate { 
get; 
set; }

public 
decimal CostUSD { 
get; 
set; }

}

伴随这个新类,我们需要在BrakAwayContext中添加一个DbSet<Trip>数据集:

public DbSet<Trip> Trips { 
get; 
set; }

我们再次运行程序,在尝试从类中创建模型时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 { 
get; 
set; }

在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(
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让数据库生成一个唯一的键值:

[Key,DatabaseGenerated(DatabaseGeneratedOption.Identity)]

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

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 { 
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

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(
8, 
1);

图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 { 
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 方法。

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 { 
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属性后:

public PersonalInfo Info { 
get; 
set; }

也需要添加一些逻辑到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 { 
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

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框架来管理你的类.

参考页面:

http://www.yuanjiaocheng.net/entity/linq-to-entities-projection.html

http://www.yuanjiaocheng.net/entity/dbset-class.html

http://www.yuanjiaocheng.net/entity/dbentityentry-class.html

http://www.yuanjiaocheng.net/entity/change-tracking.html

http://www.yuanjiaocheng.net/entity/Persistence-in-EF.html

时间: 2024-10-05 21:14:32

【译著】Code First :使用Entity. Framework编程(3)的相关文章

Code First配合Entity Framework Power Tools Beta 4使用

基于现有数据库生成POCO数据类和数据库上下文需要借助Visual Studio一个扩展插件-- Entity Framework Power Tools(一个Code First反向工程工具).只要在Visual Studio扩展里面输入“Entity Framework Power”搜索即可找到最新的扩展,点击下载即可(如下图).当然你也可以到这里Entity Framework Power Tools Beta4下载安装包进行安装. Code First配合Entity Framework

【译著】Code First :使用Entity. Framework编程(1)

参考页面: http://www.yuanjiaocheng.net/entity/Persistence-in-EF.html http://www.yuanjiaocheng.net/entity/crud-in-connected.html http://www.yuanjiaocheng.net/entity/crud-in-Disconnected.html http://www.yuanjiaocheng.net/entity/add-entity-in-disconnected.h

【译著】Code First :使用Entity. Framework编程(6)

小分享:我有几张阿里云优惠券,用券购买或者升级阿里云相应产品最多可以优惠五折!领券地址:https://promotion.aliyun.com/ntms/act/ambassador/sharetouser.html?userCode=ohmepe03 Chapter6 Controlling Database Location,Creation Process, and Seed Data 第6章 控制数据库位置,创建过程和种子数据 In previous chapters you have

Entity Framework Code First学习系列

Entity Framework Code First学习系列目录 Entity Framework Code First学习系列说明:开发环境为Visual Studio 2010 + Entity Framework 5.0+MS SQL Server 2012,在数据库方面Entity Framework Code First在Entity Framework 5.0仅支持MS SQL Server数据库.在接下来的随笔中,均使用项目名称为Portal的控制台应用程序为例.具体的系统学习目

Entity Framework 入门

Entity Framework的全称是ADO.NET Entity Framework,是微软开发的基于ADO.NET的ORM(Object/Relational Mapping)框架. Entity Framework的主要特点: 1. 支持多种数据库(Microsoft SQL Server, Oracle, and DB2): 2. 强劲的映射引擎,能很好地支持存储过程: 3. 提供Visual Studio集成工具,进行可视化操作: 4. 能够与ASP.NET, WPF, WCF, W

[VS2010] ADO.NET Entity Framework 新功能:永续保存无知对象 (Persistence-Ignorant Object) Overview

ADO.NET Entity Framework 的新功能:永续保存无知对象.可以说是 Entity Framework 划时代的新功能,颠覆一般的数据组件/DAL 与数据库间的交互方式. 前一篇文章介绍了 ADO.NET Entity Framework 的模型优先设计 (Model First Design) 功能,有没有觉得 .NET Framework 4.0 中的 ADO.NET Entity Framework 进步了很多呢?如果你这样就满足了,那接下来的东西你看了可能会像某啤酒广告

Entity Framework Code First (三)Data Annotations

Entity Framework Code First 利用一种被称为约定(Conventions)优于配置(Configuration)的编程模式允许你使用自己的 domain classes 来表示 EF 所依赖的模型去执行查询.更改追踪.以及更新功能,这意味着你的 domain classes 必须遵循 EF 所使用的约定.然而,如果你的 domain classes 不能遵循 EF 所使用的约定,此时你就需要有能力去增加一些配置使得你的 classes 能够满足 EF 所需要的信息. C

Entity Framework 5.0 Code First全面学习

目录(?)[+] 不贴图片了,太累. Code First 约定 借助 CodeFirst,可通过使用 C# 或Visual Basic .NET 类来描述模型.模型的基本形状可通过约定来检测.约定是规则集,用于在使用 Code First 时基于类定义自动配置概念模型.约定是在 System.Data.Entity.ModelConfiguration.Conventions 命名空间中定义的. 可通过使用数据注释或Fluent API 进一步配置模型.优先级是通过 Fluent API 进行

Entity Framework With Mysql 之Code First

Entity Framework 4.0现在也可以支持Mysql数据库了,这篇文章将向你展示如何用Code First的方式来实现. 1.首先新建一个项目,在项目中用NuGet添加如下引用: 2.在web.config文件中添加如下配置: 1 <connectionStrings> 2 <add name="MyTestContext" connectionString="Data Source=127.0.0.1;port=3306;Initial Cat