井底之蛙

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

第4章

对关系使用默认规则与配置

在第3章,你已经掌握了默认规则与配置对属性以及其在数据库映射的字段的影响。在本章,我们把焦点放在类之间的关系上面。这包括类在内存如何关联,还有数据库中的外键维持等。你将了解控制多重性关系,无论是否是必须的,还将学习级联删除操作。你会看到默认行为以及如何使用Data Annnotations和Fluent API来控制关系。

你会看到很多只能使用Fluent API而不能使用Data Annotations的情况。上一章我们介绍过"映射到非Unicode数据库类型"就只能在Fluent API中找到。在前几章你已经看到了几个有关默认关系的例子,如代码4-1,就是通过建立类型为List<Lodging>的Lodging属性与炻Destination建立了联系。

Example 4-1. The Destination class with a property that points to the Lodging class

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 List<Lodging> Lodgings { get; set; }

}

在Lodging类(代码4-2)也有一个Destination属性代表单个Destination实例。

Example 4-2. The Lodging class with its reference back to the Destination class

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; }

}

 

Code First观察到您既定义了一个引用又有一个集合导航属性,因此引用默认规则将其配置为一对多关系。基于此,Code First可以确定Lodging(外键)与Destination(主键)具有依赖关系。因此获表Lodging需要要一个外键映射到Destination的主键。在第2章你已经看到,在Lodgings表中确实建立了Destination_DestinationId外键字段。

本章将全面解析Code First在处理关系的默认规则以及如何按我们的意图覆写这些规则。

应用程序逻辑中的关系 
一旦Code First已经创建了模型与关系,EF框架就会将这些关系视为与使用EDMX文件映射的POCO是类似的。所有你在使用POCO对象对EF框架编程的方法和规则仍然适用。例如,如何有一个外键属性和一个导航属性关系,EF框架就会保持他们的同步。如果存在双向关系,EF框架也同样会保持他们的同步。EF框架在什么点上同步值取决于您是否在利用动态代理。没有代理,EF框架将会隐式或显示调用DetectChanges。使用代理,同步的响应发生在属性值变更的时候。事实上你不需要关心是否调用DetectChanges因为DbConext将会在你调用任何依赖同步的方法自动调用。EF框架开发团队建议你如果需要只用动态代理;通常这都是围绕着性能调优进行的。没有代理的POCO类通常使交互关系理简化,因为你没必要知道代理相关的附加行为。 

多重性关系

如前所述,Code First在看到导航属性和可选的外键属性时将创建关系。有关导航属性和外键属性的细节将帮助我们来确定多重关系的每一端。本章对外键将关注更多一点;现在我们来看看在类中没有外键属性定义的情况。

Code First在处理多重性关系时应用了一系列规则。规则使用导航属性确定多重性关系。即可以是一对导航属性互相指定(双向关系),也可以是单个导航属性(单向关系)。

•如果你的类中包含一个引用和一个集合导航属性,Code First视为一对多关系;

• 如果你的类中仅在单边包含导航属性(即要么是集合要么是引用,只有一种),Code First也将其视为一对多关系;

• 如果你的类包含两个集合属性,Code First默认会使用多对多关系;

•如果你的类包含两个引用属性,Code First会视为一对一关系;

•在一对一关系中,你需要提供附加信息以使Code First获知何为主何为辅。本章后面会在"一对一关系"中提到。如果没有在类中定义外键属性,Code First将设定关系为可选(即一端的关系实际是零对一或恰好相反,零指的是可空---译者注)。

•在本章的"外键"小节,你会看到当在类中定义外键属性,Code First会使用属性的可空性来确定关系是必须的还是可选的。

回顾我们刚刚重温的Lodging与destination的关系,你会看到上述规则。由于有集合和引用属性,Code First就将其视为一对多关系。同时也看到,通过默认规则,Code First将经将其配置为可选关系(optional)。但是在我们的场景里,确实没有想让一个Lodging(住所)不从属于一个Destination(目的地)。因此我们来看看如何确保这种关系是必须的。

使用Data Annotations配置多重关系

大多数多重关系配置都需要使用Fluent API。但是我可以使用Data Annotations来指定一些关系是必须的。只需要简单地将Required标记放在你需要定义为必须项的引用属性上。修改Lodging类的代码将Required特性标记放在Destination属性上(代码4-3):

Example 4-3. Required annotation added to Destination property

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; }

[Required]

public Destination Destination { get; set; }

}

运行程序,数据库重新创建,你会看到Lodgings表中Destination_DestinationId不再允许空值(图4-1)。这是因为关系现在是必须的(Required)。

使用Fluent API配置多重性关系

如果没有花时间去理解基本原理,使用Fluent API配置关系会让人感到迷惑。

当使用Data Annotations修改关系时,可以将特性直接放在了导航属性上。这与Fluent API不同,Fluent API并不直接在属性上配置关系。为了达到相同的目的,必须先确定关系。有时在一端就足够,但更多的需要对全部关系进行描述。

为了确定关系,你必须指明导航属性。不管从哪端开始,都要使用这样的代码模板:

 

Entity.Has[Multiplicity](Property).With[Multiplicity](Property)

 

多重性关系可以是Optional(一个属性可拥有一个单个实例或没有),Required(一个属性必须拥有一个单个实例)或很多的(一个属性可以拥有一个集合或一个单个实例)。

Has方法包括如下几个:

• HasOptional

• HasRequired

• HasMany

在多数情况还需要在Has方法后面跟随如下With方法之一:

• WithOptional

• WithRequired

• WithMany

代码4-4显示了一个 使用现有的Destination和Lodging之间的一对多关系的实例。这一配置并非真的做任何事,因为这会被Code First通过默认规则同样进行配置。本章后面会看到识别这种关系然后作进一步的配置,实现外键关系和级联删除功能。

Example 4-4. Specifying an optional one-to-many relationship

 

modelBuilder.Entity<Destination>()
.HasMany(d => d.Lodgings)
.WithOptional(l => l.Destination);

 

 

这一代码确定Destination的Has关系。有很多由Lodgings的属性所定义的关系。Lodgings端到Destination的关系是可选的。图4-2尝试帮你观察这种关系建立的过程。

我们来看看如何使用Fluent API建立Required关系。在DestinationConfiguration添加代码4-6:

Example 4-6. Configuring a required relationship with the Fluent API

 

HasMany(d => d.Lodgings)
.WithRequired(l => l.Destination);

 

这看起来非常类似于代码4-5,只不过调用了HasRequired来取代HasOptional。这会使Code First知晓你想建立一个必须的(Required)一对多关系。运行程序你会看到数据库与图4-1显示的一样,与使用Data Annotations的Required标记产生效果一致。如果你想在两端配置全必须的一对一或全可选的一对一关系,Code First会需要更多的信息来获知何为主何为辅。这种Fluent API代码会让人很迷惑!好消息是你可能不需要经常这么做。这一议题将在"1-1关系"中详细讲述。 

 

使用外键

到目前为止,我们只是看了在类中没有外键属性的关系。例如,Lodging只包含一个引用属性到Destination,但没有属性来存储它指向Destination的键值。在这种情况下,我们已经看到,Code First会为你的数据库引入外键。我们来看看在如果在类本身引入键属性时会发生什么。

在上一节中,通过添加一些配置,建立了LodgingDestination的Required关系。请删除此配置,以使我们可以观察到Code First的约定行为。配置删除后,添加到一个DestinationId属性到Lodging类中:

public int DestinationId { get; set; }

一旦你添加外键属性到Lodging类,继续运行您的应用程序。该数据库将回应你刚才的改变重新创建。如果您检查Lodgings表列,你会发现,Code First自动检测DestinationId是一个外键,对应于LodgingDestination的关系,不再产生Destination_DestinationId外键(图4-3)。

正如您现在可能期望的,Code First有一个设置或规则得到了应用,结果就是发现了一个关系尝试并找到一个外键属性。规则使用的是属性的名称。按照默认规则,一旦发现外键属性,就被命名为“[目标类型的键名][目标类型名称]+[目标类型键名称]”,或“[导航属性名称]+[目标类型键名称]”的形式。前面提到的三个规则中的第一个与您添加的属性DestinationId相匹配。名称匹配是区分大小写的,所以你可以有一个名为DestinationIDDeStInAtIoNiD,或任何其他变化的属性,(将不会被匹配,译者注)。如果没有检测到外键,也没有配置,Code First会自动在数据库中设置一个。

为什么要使用外键属性? 
在编写代码时,要找出一个与其他类的关系。例如,您可能会创建一个新的Lodging,要指定Lodging与哪个Destination相关。如果特定的Destination在内存中,你就可以通过导航属性的设置关系: 
myLodging.Destination=myDestinationInstance; 
但是,如果Destination不在内存中,这将要求你先执行对数据库的查询,检索Destination,让你可以设置该属性。有时,你可能没有在内存中的对象,但你想访问该对象的键值。
带有外键的属性,你可以简单地使用键值而不依赖于内存中的实例: 
myLodging.DestinationId=3; 
此外,在特定情况下,如果Lodging是新建的,您可以附加到原有的Destination实例上,有些情况下,实体框架还要设定Destination的状态为新增,即使所需的Destination实例已经
存在于数据库中。如果你只与外键进行工作,就能避免这个问题。

还有一些有趣的现象会在添加外键属性时会发生。没有 DestinationId外键属性时,Code First的约定规则允许Lodging.Destination是Optional,这意味着你可以添加没有DestinationLodging。如果回到第2章中的图2-1,你会看到,在Lodgings表中Destination_DestinationId字段为可空类型。现在DestinationId属性加入,数据库中的字段不再是可空的,你会发现,你不再可以保存没有Destination,或没有DestinationId属性填充的Lodging数据。这是因为DestinationIdint类型,这是一个值类型,不能分配null值。如果DestinationId类型是Nullable<int>的,这种关系将保持Optional状态。事实上,Code First默认约定就是根据类中外键属性的可空性,来确定是否关系是Required还是Optional的。

Code First允许你定义的类中不使用外键属性建立关系,只是使用外键属性更容易建立关系。然而,由于没有外键属性可以依赖,开发者在与实体框架中的相关数据工作时会遇到一些混乱的行为。如果有外键属性,EF框架会在执行插入时检查关系约束,可以避免插入无效数据。没有外键属性来对一个要求为Required的主实体进行跟踪时(例如,要求一个特定的destination必须对应特定的lodging)时,就需要开发者自己来确保以某种方式提供所需信息给EF(很显然,这样更麻烦---译者注)。您还可以了解更多有关"缺少外键下工作"的信息,见2012年1月号(http://msdn.com/magazine) 。

指定非规则命名的外键

如果有一个不遵循规则的外键会怎么样呢?

我们来引入一个新的InternetSpecial类,来跟踪一些各种lodging的特定价格(代码4-7)。这个类即有导航属性(Accommodation),又有外键属性(AccommodationId),都是为同一关系设立的。

Example 4-7. The new InternetSpecial class

using System;

namespace Model

{

public class InternetSpecial

{

public int InternetSpecialId { get; set; }

public int Nights { get; set; }

public decimal CostUSD { get; set; }

public DateTime FromDate { get; set; }

public DateTime ToDate { get; set; }

public int AccommodationId { get; set; }

public Lodging Accommodation { get; set; }

}

}

在Lodging中需要一个新的属性来包含每个logding的特定报价。

public List<InternetSpecial> InternetSpecials { get; set; }

Code First看到Lodging有很多InternetSpecials,而InternetSpecials又有一个Lodging(称之为Accommodation).尽管没有设置DbSet<InternetSpecial>, InternetSpecial也可以通过Lodging而包含在模型里。

再次运行程序,将会创建如图4的表。不仅有不是外键的AccommodataionId列,也新增了一个外键列,Accommodation_LodgingId。

你会看到Code First引入一个外键。Code First根据Accommodation导航属性,检测到了一个对应对Lodging的关系然后使用默认规则创建了Accommodation_LodgingId字段。默认规则无法将AccommodationId推断为外键,因为Code First检查了默认规则对外键属性名称的三个要求没有在类中找到匹配项,就创建了自己的外键。

使用Data Annotations修改外键

你可以使用 Data Annotations 的配置外键特性ForeignKey来声明外键属性。在AccommodtaionId上添加ForeignKey特性告知Code First哪个导航属性是外键,来修复这个问题。

[ForeignKey("Accommodation")]

public int AccommodationId { get; set; }

public Lodging Accommodation { get; set; }

你也可以将ForeignKey特性放在导航属性上来通知哪个属性是关系的外键。

public int AccommodationId { get; set; }

[ForeignKey("AccommodationId")]

public Lodging Accommodation { get; set; }

两种写法都可以。与此同时,你获得的正确的的数据库外键:AccommodtationId,如图4-5所示。

使用Fluent API来修改外键

 

Fluent API并没有提供配置属性作为外键的简单方法。你要使用专门的关系API来配置正确的外键。而且你不能简单地配置关系的片断,你需要首先指定你想配置的关系类型(前面已经提到)然后才能应用修改。

为了指定关系,需要从IneternetSpecial实体开始,我们直接在modelBuilder中进行配置,当然也你可以在EntityTypeConfiguration类中为InternetSpecial创建一个实例。

在这种情况下,我们先要设置关系而不打破Code First建立的默认关系。代码4-8指出了这种关系:

Example 4-8. Identifying the relationship to be configured

modelBuilder.Entity<InternetSpecial>()

.HasRequired(s => s.Accommodation)

.WithMany(l => l.InternetSpecials)

我们想要改变的,是在这种关系下的外键。Code First期待外键属性命名为LodgingId或者是其他的默认名称。因此我们需要告诉AccommodationId 属性才是真正的外键:.代码4-9添加了HasForeignKey方法来为关系指定外键:

Example 4-9. Specifying a foreign key property when it has an unconventional name

modelBuilder.Entity<InternetSpecial>()

.HasRequired(s => s.Accommodation)

.WithMany(l => l.InternetSpecials)

.HasForeignKey(s => s.AccommodationId);

 

效果与图4-5一致。

Working with Inverse Navigation Properties

使用逆导航属性

Code First到目前为止一直能够解析我们定义的两个导航属性,虽然它们处于不同端,实际上是同一关系。它之所以能做到这一点,因为两端至少有一个可能的匹配关系。例如,Lodging只包含一个单一的属性,指向目的地(Lodging.Destination;同样地,目的地Destination只包含一个属性引用住所(Destination.Lodgings)。

虽然并不十分普遍,您可能会遇到这样一种情况:实体之间存在多个关系。在这种情况下,Code First将不能够与相关导航属性相匹配。您将需要提供一些额外的配置。

例如,如果你想跟踪每个住所的两个联系人怎么办?这就需要在Lodging类中有一个PromaryContact和一个SecondaryContact属性。我们先将这两个属性添加到类中:

public Person PrimaryContact { get; set; }

public Person SecondaryContact { get; set; }

 

在关系的另一端我们也需要引入导航属性。这需要让你从Person类导航到Lodging实例,知道第一联系人和第二联系人连接到哪里去。添加如下两个属性到Person类:

public List<Lodging> PrimaryContactFor { get; set; }

public List<Lodging> SecondaryContactFor { get; set; }

 

Code First默认约定将对你刚才添加的这些新的关系进行错误的假设。因为有两套导航属性,Code First无法确定他们如何匹配,它会创建单独为每个属性创建关系。图4-6显示的Code First创建的基于您刚才添加的导航属性的四个关系。

Code First默认规则可以识别双向关系,但不能识别在两个实体中多个双向关系。原因如图4-6所示,多个外键,使得Code First无法确定在Lodging中的两个返回Person实体的属性连接到Person类的哪个List<Lodging>属性。

你可以添加配置(使用Data Annotations或Fluent API)到modelBuilder来表明这种关系。使用Data Annotations,你需要使用一个特性标记叫做InverseProperty。使用Fluent API,需要合并使用Has/With方法指定这些关系正确的端点。

你可将特性标记放在关系的任何一端(或两端都放)。我们将其放在Lodging类中(代码4-10)。InverseProperty特性标记需要相关类中相应导航属性作为参数。

Example 4-10. Configuring multiple bidirectional relationships from Lodging to Person

[InverseProperty("PrimaryContactFor")]

public Person PrimaryContact { get; set; }

[InverseProperty("SecondaryContactFor")]

public Person SecondaryContact { get; set; }

使用Fluent API,你需要使用Has/With语句来指定关系的两端。见代码4-11 ,第一个配置的一端为Lodging.PrimaryContact,另一端为Person.Primary ContactFor。第二个配置是针对SecondaryContact和SecondaryContactFor两者关系建立的,方法类似。

Example 4-11. Configuring multiple relationships fluently

modelBuilder.Entity<Lodging>()

.HasOptional(l => l.PrimaryContact)

.WithMany(p => p.PrimaryContactFor);

modelBuilder.Entity< Lodging >()

.HasOptional(l => l.SecondaryContact)

.WithMany(p => p.SecondaryContactFor);

使用级连删除

级联删除允许主记录被删除时相关联的依赖性数据也被删除。例如,如果你删除Destinantion,相关的Lodgings会被自动删除。EF框架支持对内存中和数据库中的数据进行级联删除。在"用EF框架编程"第二版第19章,推荐为模型实体配置级联删除,所映射的数据库对象也会具有级联删除的定义。

默认规则约定,Code First会对Required的关系设置级联删除。当一个级联删除定义后,Code First会在数据库中为其创建级联删除。在本章前面我们已经将Lodging和Destination的关系设定为Required 。换句话说,没有Destination,Lodging也不存在。因此,如果删除一个Destination,任何相关联的Lodging(在内存中且被上下文所跟踪)也会被删除。当提交SaveChanges,数据库会删除任何保存在Lodgings表中的相关行,使用的就是级联删除行为。

再看数据库,你会看到Code First实施了级联删除并且在数据库之间的关系上添加了约束。请注意图4-7的删除规则设定了级联。

 

代码4-12是一个新方法叫做DeleteDestinationInMemoryAndDbCascade,用于展示内存中和数据库中的级联删除。

Example 4-12. A method to explore cascade deletes

private static void DeleteDestinationInMemoryAndDbCascade()

{

int destinationId;

using (var context = new BreakAwayContext())

{

var destination = new Destination

{

Name = "Sample Destination",

Lodgings = new List<Lodging>

{

new Lodging { Name = "Lodging One" },

new Lodging { Name = "Lodging Two" }

}

};

context.Destinations.Add(destination);

context.SaveChanges();

destinationId = destination.DestinationId;

}

using (var context = new BreakAwayContext())

{

var destination = context.Destinations

.Include("Lodgings")

.Single(d => d.DestinationId == destinationId);

var aLodging = destination.Lodgings.FirstOrDefault();

context.Destinations.Remove(destination);

Console.WriteLine("State of one Lodging: {0}",

context.Entry(aLodging).State.ToString());

context.SaveChanges();

}

}

 

代码使用context插入了一个新Destination,有两个Lodging.然后将这些Lodging储存进数据库然后记录了新添加的Destination。在一个单独的context里,代码取出Destination和其相关的Lodging,然后使用Remove方法标记Destination实例为删除,我们使用Console.WriteLine来检测相关Lodging实例在内存中状态,这使用了一个DbContext的Entry方法。Entry方法能够让我们访问EF施加给给定对象的状态信息。最后,调用SaveChanges方法持久化删除信息到数据库。

调用Destination的Remove方法,Lodging的状态显示在控制台窗口。尽管我们并没有显示地要求删除任何Lodging,但仍显示出了删除命令。这是因为当我们显示地删除Destination时,EF框架使用客户端的级联删除功能删除了依赖的Lodging。

下一步,当SaveChanges方法调用时,EF框架发送三个DELERE命令到数据库。如图4-8所示,前两删除命令是对相关Lodging实例进行删除,第三个才是删除Destination,

现在我们来改变一下方法。我们将要要随同Destination一起删除以前存入的Loading数据。我们删除与Lodging提到的所有相关代码。由于内存中无Lodging,就不会有客户端的级联删除,而数据库却清除了任何孤立的Lodgings数据,这是因为在数据库中定义了级联删除。(见图4-7)修改的方法见代码4-13.

Example 4-13. Modified DeleteDestinationInMemoryAndDbCascade code

private static void DeleteDestinationInMemoryAndDbCascade()

{

int destinationId;

using (var context = new BreakAwayContext())

{

var destination = new Destination

{

Name = "Sample Destination",

Lodgings = new List<Lodging>

{

new Lodging { Name = "Lodging One" },

new Lodging { Name = "Lodging Two" }

}

};

context.Destinations.Add(destination);

context.SaveChanges();

destinationId = destination.DestinationId;

}

 

using (var context = new BreakAwayContext())

{

var destination = context.Destinations

.Single(d => d.DestinationId == destinationId);

context.Destinations.Remove(destination);

context.SaveChanges();

}

using (var context = new BreakAwayContext())

{

var lodgings = context.Lodgings

.Where(l => l.DestinationId == destinationId).ToList();

Console.WriteLine("Lodgings: {0}", lodgings.Count);

}

}

运行后,发送到数据库的唯一命令是删除destination。数据库级联删除响应的相关Lodging。当在Lodgings端查询时,由于数据库删除了lodgings,查询不会返回结果,lodgings变量成为一空的列表。

使用Fluent API配置打开或关闭客户端级联删除功能

你可能会在现有的数据库上工作,不使用级联删除或者你可能有一个规则必须显示删除数据,不允许在数据库中自动删除。如果从LodgingDestination之间的关系是Optional,这不是一个问题,因为按照默认规则,Code First不能在可选的关系上使用级联删除。但你可能需要即有Required的关系,又不想使用级联删除功能。

例如需要在应用程序中试图删除一个Destination时向用户返回一个错误,这是在没有显示地删除或重新给这个Destination分配Lodging实例时出现的。在这种情况下,你就需要一个Required关系而不需要级联删除,你可以显示地覆写默认规则通过使用Fluent API来对级联删除进行配置。这个功能Data Annotations不支持。

记住,如果建立这样的模型,应用程序代码可以实现定制地删除数据,或在必要时重新分配相关的数据。

Fluent API使用的方法是WillCascadeOnDelete,以一个布尔值作为参数。此配置适用于所有关系,因此首先需要使用指定一个配对的关系,然后调用WillCascadeOnDelete方法。

在LodgingConfiguration类中,关系定义为:

HasRequired(l=>l.Destination)

.WithMany(d=>d.Lodgings)

在这里,有三个可能的配置可供添加。WillCascadeOnDelete是其中之一,如图4-9所示。

现在你可以设置此关系的WillCascadeOnDelete为false:

HasRequired(l=>l.Destination)

.WithMany(d=>d.Lodgings)

.WillCascadeOnDelete(false)

这就使得Code First生成的数据库架构将不会包含级联删除。图4-7所示的级联删除规则将不会出现。

在关系为Required的场景下,这种逻辑会创建一个冲突,例如,目前在LodgingDestination的Required关系中,需要一个Lodging实例有一个Destination或一个DestinationId。如果你有一个正在变化的跟踪,并删除了相关的Destination,这将导致Lodging.Destination为空。调用SaveChanges时,实体框架将尝试同步Lodging.DestinationId,设置为NULL。但是,这是不行的,异常将抛出下面的详细信息:

The relationship could not be changed because one or more of the foreign-key properties is non-nullable. When a change is made to a relationship, the related foreign-key property is set to a null value. If the foreign-key does not support null values, a new relationship must be defined, the foreign-key property must be assigned another non-null value, or the unrelated object must be deleted.

关系不能更新,因为一个或多个外键的属性非空。对关系进行更新时,相关的外键的属性设置为空值。如果外键不支持空值,必须定义一个新的关系,外键的属性必须指派另一个非空值,或删除非关联的对象。

这表明,如果已经控制了级联删除设置,就要为避免或解决验证没有级联删除可能引起的冲突负责。

对不被数据库所支持的场合关闭级联删除

许可数据库(包括SQL Server)不支持指定级联删除指向到同一个表的多重关系(原文为:Some databases (including SQL Server) don't support multiple relationships that specify cascade delete pointing to the same table.不知如何翻译---译者注 )。由于Code First配置的Required关系包括级联删除,这就造成如果有两个Required关系指向同一个实体就会出现错误。你可以使用WillCascadeOnDelete(false)来关闭一个或多个联删除设置。代码4-14显示了如果不进行正确配置来自于SQL Server的异常信息。

Example 4-14. Exception message when Code First attempts to create cascade delete where multiple relationships exist

System.InvalidOperationException was unhandled

Message=The database creation succeeded, but the creation of the database objects

did not.

See InnerException for details.

InnerException: System.Data.SqlClient.SqlException

Message=Introducing FOREIGN KEY constraint 'Lodging_SecondaryContact' on table

'Lodgings' may cause cycles or multiple cascade paths. Specify ON DELETE

NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY

constraints. Could not create constraint. See previous errors.

 

客户端级联删除对性能的影响

本身就会实施级联删除。如果你将所有相关对象调入内存,让客户端级联删除这些对象,就会导致调用SaveChanges方法时,会发送很多针对这些相关对象的Delete命令到数据库,从而造成暂时的命令拥塞。当然有的情况下,这些相关对象是在内存中的,也希望这些对象能够被删除,这种情况另当别论。但是,如果数据并没载入内存,完全可以依靠数据库做级联删除,而避免将这些对象载入内存。

探索多对多关系

EF框架支持多对多关系。让我们来看看Code First是如何在生成数据库时响应类间的多对多关系。

在使用database first策略时如果有多对多关系,EF框架可以创建多对多映射,条件是数据库内联表只包含相关实体的主键。这种映射规则也适用于Code First。

我们添加一个新的类:Acitivity到模型中,如代码4-15,将于Trip类相关联。一个Trip类可以有一些Activites日程,而一个Activity日程又可以计划好几个trips(行程)。因此Trip和Activity就会有多对多关系。

Example 4-15. A new class, Activity

using System.ComponentModel.DataAnnotations;

using System.Collections.Generic;

namespace Model

{

public class Activity

{

public int ActivityId { get; set; }

[Required, MaxLength(50)]

public string Name { get; set; }

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

}

}

在Activity类中有一个List<Trip>,我们也添加了一个List<Activity>到Trip类到另一端形成多对多关系。

public List<Activity> Activities { get; set; }

再次运行程序,因为模型变化Code First将重新创建数据库。Code First根据默认规则识别出了多对多关系,建立了内联表,并配置了合适的键。两个内联表的主键都作为外键指向了内联表,如图4-10所示。

注意到Code First的默认规则创建的表名合并使用了两个类的类名。它也使用了我们在前面创建外键使用的模式来创建外键。在第5章,我们将关注于表和列的映射,到时你会学习到如何使用配置为内联表指定表名和列名。

一旦多对对关系建立,其行为就与EF早期版本中多对多关系所表现出来的是一样的。你可以通过类属性查询,添加和删除相关对象。在后台,EF框架将使用它的内置特性来协助数据库创建集成的内联表的select,insert,update和delete命令。

例如,如下的查询寻找一次单独的trip和计划实施的相关Activities.

var tripWithActivities = context.Trips

.Include("Activities").FirstOrDefault();

查询是对类进行的,没有必要关心trip和activities是怎样在数据库连接的。EF框架会自行配置SQL语句执行内联,并返所有适合于第一条trip的所有activities记录。虽然不需要自行构建SQL语句,但一定要记住不管你的类的结构或数据库构架如何,EF框架构建的SQL都是可以通用的。

输出的结果是trip和其activities的图。图4-11显示了Trip类在一个调试窗口的信息。你可以看到其包含两个activites,都最从数据库中提取出来匹配这次Trip的。

不必知道它的存在,EF框架会维护内联表并通过来组织表之间的Join。同样地,任何时候你进行插入,更新或删除操作,EF框架将制定出正确的内联SQL语句,不用在你的代码中作任何关注。

使用单边导航的关系

到目前为止我们已经观察了导航属性已经定义在两个类中的关系。但是,这并不是EF框架能够工作所必须的。

在你的域中,从Destination导航到其相关的Lodging选项是一种通常的情况,但是可能很少需要从Lodging导航回Destination.让我们将Destination从Lodging类中移走(代码4-16)。

Example 4-16. Navigation property removed from Lodging class

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 int DestinationId { get; set; }

//public Destination Destination { get; set; }

public List<InternetSpecial> InternetSpecials { get; set; }

public Person PrimaryContact { get; set; }

public Person SecondaryContact { get; set; }

}

EF框架可以处理这种情况。这里清晰地定义了从Lodging到Destination之间的关系,依据的是Destination类中的Lodgings属性。这仍然会使用模型构建器到Lodging类中去寻找外键Lodging.DestinationId满足默认规则。

现在我们前进一步,将Lodging类中的外键属性删除,如代码4-17.

Example 4-17. Foreign key commented out

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 int DestinationId { get; set; }

//public Destination Destination { get; set; }

}

是否还记得如果不定义一个外键在你的类中Code First默认规则会自动引入一个?同样的规则适用于在单边定义的导航属性。Destination仍然有一个属性定义 了到Lodging的关系。图4-13显示了有一个Destination_DestinationId列加入到的Lodgings表中。这可能会使你回想起有关外键列的命名规则:[Navigation Property Name] + [Primary Key Name]。但是我们在Lodgin类里不再有一个导航属性。如果在依赖实体中没有导航属性加以定义,Code First将会使用[Principal Type Name] + [Primary KeyName].在这种情况下,使用了同一个名字。

那么如果我们试图在另一个类中只定义外键而没有导航属性呢,EF框架本身支持这种情况,但Code First不支持。Code First需要至少一个导航属性来创建关系。如果你移除了两边的导航属性,Code First将只将外键属性作为任何类中的其他属性而不会在数据库中创建约束。

现在我将外键属性调整为默认规则无法检测到的情况。我们用LocationId替代DestinationId,如代码4-16.记住我们没有导航属性,仍然被注释着。

Example 4-18. Foreign key with unconventional name

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 int LocationId { get; set; }

//public Destination Destination { get; set; }

public List<InternetSpecial> InternetSpecials { get; set; }

public Person PrimaryContact { get; set; }

public Person SecondaryContact { get; set; }

}

感谢Destination.Lodgings,Code First知道两个类中存在关系。但它无法找到一个符合约定的外键。我们之前已经铺了路,现在还需要一些配置来帮助Code First识别外键。

在前面的例子里,我们将ForeignKey特性标记放在依赖类的导航属性或者将其放在外键属性上,告知哪个导航属性属于它。但我们在依赖类中不再有一个导航属性。幸运的是,我们可以将Data Annotations的标记放在导航属性上(Destination.Lodgings)。Code First知道Lodging是关系中的依赖类,因此它会为外键在此类中寻找有关字段:

[ForeignKey("LocationId")]

public List<Lodging> Lodgings { get; set; }

Fluent API也能为这种单侧导航属性创建关系。配置的Has部分必须指定一个导航属性,而With部分如果没有反向导航属性就留空。一旦指定了Has和With语句,就可以调用HasForeignKey方法:

modelBuilder.Entity<Destination>()

.HasMany(d => d.Lodgings)

.WithRequired()

.HasForeignKey(l => l.LocationId);

在我们需要创建单边关系时,很多情况下我们想要从Lodging导航回相应的Destination。我们恢复对Lodging类的调整。取消对Destination属性的注释并将外键属性恢复,如代码4-19.你也需要将ForegnKey标记从Destination.Lodging上移除,删除上述刚刚添加的Fluent API配置。

Example 4-19. Lodging class reverted to include navigation property and conventional foreign key

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 int DestinationId { get; set; }

public Destination Destination { get; set; }

public List<InternetSpecial> InternetSpecials { get; set; }

public Person PrimaryContact { get; set; }

public Person SecondaryContact { get; set; }

}

使用一对一关系

There is one type of relationship that Code First will always require configuration for: one-to-one relationships. When you define a one-to-one relationship in your model, you use a reference navigation property in each class. If you have a reference and a collection, Code First can infer that the class with the reference is the dependent and should have the foreign key. If you have two collections, Code First knows it's many-to-many and the foreign keys go in a separate join table. However, when Code First just sees two references, it can't work out which class should have the foreign key.

Let's add a new PersonPhoto class to contain a photo and a caption for the people in the Person class. Since the photo will be for a specific person, we'll use PersonId as the key property. And since that is not a conventional key property, it needs to be configured as such with the Key Data Annotation (Example 4-20).

有一种关系Code First必须进行配置后才能工作,这种关系就是一对一关系。当你在模型中定义一对一关系,你需要在每个类中都要使用引用导航。如果你有一个引用和一个集合,Code First就会将引用视为依赖类,推测应该有一个外键。如果有两个集合,Code First视为多对多关系,将外键放在一个单独的内联表中。但是,Code First看到两个引用时,它无法识别哪个类应该有一个外键。

我们添加一个新的PersonPhoto类,包含一个针对属于Person类中的people的photo和caption属性。由于photo将会指定给特定的person,我们使用PersonId作为键属性。并有没有一个默认的键属性,需要如下所示的Data Annotations配置(代码4-20):

Example 4-20. The PersonPhoto class

using System.ComponentModel.DataAnnotations;

namespace Model

{

public class PersonPhoto

{

[Key]

public int PersonId { get; set; }

public byte[] Photo { get; set; }

public string Caption { get; set; }

public Person PhotoOf { get; set; }

}

}

我们在Person中也添加一个Photo属性,这样可以在两端都可以导航。

public PersonPhoto Photo { get; set; }

记住在这种情况下Code First无法确认哪个类是依赖类。当其尝试构建模型时,就会抛出一个异常,告知你它需要更多信息:

Unable to determine the principal end of an association between the types 'Model.PersonPhoto' and 'Model.Person'. The principal end of this association must be explicitly configured using either the relationship fluent API or data annotations.
无法确认在类型'Model.PersonPhoto'和'Model.Person'之间联系的主端。这种联系的主端必须使用Fluent API或data annotations进行显示配置;

个问题可以很容易地使用ForeignKey特性标记来解决,将标记放在依赖类上指出其包含外键。当配置一对一关系时,EF框架需要依赖类的主键也应是外键。在我们的案例中,PersonPhoto是依赖类,而其键,PersonPhoto.PersonId,也应是一个外键。我们将ForeignKey标记加在PersonPhoto.PersonId属性上,如代码4-21,记住在加入ForeignKey时要为关系指定导航属性。

 

Example 4-21. Adding the ForeignKey annotation

public class PersonPhoto

{

[Key]

[ForeignKey("PhotoOf")]

public int PersonId { get; set; }

public byte[] Photo { get; set; }

public string Caption { get; set; }

public Person PhotoOf { get; set; }

}

运行程序会成功创建新数据库标,尽管你会看到EF框架并没有很好地处理单词"Photo",还是将其复数化(第5章你会学习如何为表指定名称),但是PersonId现在即是PK又是FK。如果你观察PersonPhoto_PhotoOf外键约束细节,还可以看到这里显示People.PersonId在关系中是主表/列,而PersonPhotoes.PersonId是外键表/列(图4-14):

在本章前面,介绍过可以将ForeignKey标记放在导航属性上,也可以指定外键属性的名称(在本例中,就是PersonId).由于两个类都包含PersonId属性,Code First仍不能确认哪个类包含外键,因此你不能用这样的方式来为此种场景配置。

当然,我们也可以Fluent API来进行配置。我们假定这时的关系是一对零或一对一,也就是PersonPhoto必须有一个Person对应而一个Person不必一定有一个PersonPhoto对应。我们使用HasRequired和WithOptinal联合使用来指定这种情况:

modelBuilder.Entity<PersonPhoto>()

.HasRequired(p => p.PhotoOf)

.WithOptional(p => p.Photo);

这足以让Code First将PersonPhoto视作依赖类。我们想要将Person类作为主类而PersonPhoto辅助类,因为一个Person可以存在没有PersonPhoto的情况,但是一个PersonPhoto必须有一个Person.

注意你没有必要使用HasForeignKey来指定PersonPhot.PersonId作为外键。这是因为EF框架可以直接将依赖项的主键作为外键使用。由于没有选择,Code First会将这种唯一情况推断出来。事实上,Fluent API也不会让你使用HasForeignKey,在HasRequired和WithOptional方法后的智能感知里该方法根本不可用。

当两端都是Required时配置一对一关系

现在我们来告诉Code First一个Person必须有一个PersonPhoto(即也是Required)。使用Data Annotations,可以将Rrequired标记放在任何类型的属性上来实现(不一定非是原生类型):

[Required]

public PersonPhoto Photo { get; set; }

现在更新Main方法来调用InserPerson方法(见第3章),运行程序。在运行SaveChanges时会抛出异常,EF框架的验证API报告对PersonPhoto的Required要求验证失败。

Ensuring that the sample code honors the required Photo

如何修正代码?

如果你想让Photo属性为Required又要避免验证错误,可以修改InsertPerson和UpdatePerson方法以便可将数据添加到Photo字段中。为了保持代码的简洁,我们只填充一个单一的字节到Photo的byte数组里而不是使用实际的图片。

在InsertPerson方法里,修改代码实例化一个新的Person对象添加Photo属性,如代码4-22:

Example 4-22. Modifying the InsertPerson method to add a Photo to the new Person

var person = new Person

{

FirstName = "Rowan",

LastName = "Miller",

SocialSecurityNumber = 12345678,

Photo = new PersonPhoto { Photo = new Byte[] { 0 } }

};

在UpdatePerson方法中,我们添加了一些代码来保证任何已添加的Person数据都会在更新时同时获得一个Photo。修改UpdatePerson方法见代码4-23:

Example 4-23. Modification to UpdatePerson to ensure existing Person data has a Photo

private static void UpdatePerson()

{

using (var context = new BreakAwayContext())

{

var person = context.People.Include("Photo").FirstOrDefault();

person.FirstName = "Rowena";

if (person.Photo == null)

{

person.Photo = new PersonPhoto { Photo = new Byte[] { 0 } };

}

context.SaveChanges();

}

}

更新方法使用Include方法来获取数据库中Person的图片。然后检查Person对象是否有Photo数据,如果没有就添加一个新的。现在Person类中的Photo的Required要求得到满足,就可以在任何时候成功执行InsertPerson和UpdatePerson方法。

使用Fluent API配置一对一关系

毫无疑问,也可以使用Fluent API来配置同样的关系。但首先需要让Code First知道哪个类为主哪个类为辅。如果两端均为Required,不能简单地从多重关系上推测出来。

你可以跟随在WidthRequired后面来调用HasRequired方法 。但是如果你开始于HasRequired,你会在WithReuired的位置有两个附加选择:WithRequiredPrincipal 和WithRequiredDependent。这些方法将你要配置的实体考虑了进去(就是你选择的基于模型构建器或者EntityTypeConfiguration类建立的实体)。选择WithRequiredPrincipal将会使实体配置为主类,意味着该类包含有关系的主键。选择WithRequiredDependent会使实体配置为辅助类,意味着该类包含有关系的外键。

假设你想将PersonPhoto配置为依赖类,应该使用下列配置代码:

modelBuilder.Entity<PersonPhoto>()

.HasRequired(p => p.PhotoOf)

.WithRequiredDependent(p => p.Photo());

配置两端都是Optional的一对一的关系其方法是类似的,除了开始于HasOptional外还应该选择是WithOptionalPrincipal 还是 WithOptionalDependent。

小结

 

在本章,你已经看到Code First在处理关系上很智能。Code First的默认规则能够发现任何多样性的关系,并适时提供外键配置。但也有一些场景可能你并不想完全遵循默认规则,Code First完全支持这种场景。你也学习了如何使用Data Annotations和Fluent API来定制模型。你应该已经很好地理解了如何在在Fluent API中使用基于Has/With的语句来处理关系。

在下一章,我们来看看Code First的另一套映射,就是类如何映射到数据库,包括如何映射到各种继承架构等。

posted on 2012-09-28 10:25  那年的初秋  阅读(4624)  评论(0编辑  收藏  举报