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

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

In Chapter 3, you learned about convention and configuration that affect attributes of properties and the effects that these have on the database. In this chapter, the focus will be on convention and configuration that affects the relationships between classes. This includes how classes relate to one another in memory, as well as the corresponding foreign key constraints in the database. You'll learn about controlling multiplicity, whether or not a relationship is required, and working with cascade deletes. You'll see the conventional behavior and learn how to control the relationships using Data Annotations and the Fluent API.

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

You'll start seeing more configuration that can be performed with the Fluent API but cannot be done through Data Annotations. Recall, however, that if you really love to apply configuration with attributes, the note in "Mapping to Non-Unicode Database Types" on page 51 points to a blog post that demonstrates how to create attributes to perform configuration that is only available through the Fluent API. You've already seen some of the relationship conventions in action throughout the earlier chapters of this book. You built a Destination class (Example 4-1) that has a Lodgings property which is a List<Lodging>.

你会看到很多只能使用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; }

}

On the other end of the relationship, the Lodging class (Example 4-2) has a Destination property that represents a single Destination instance.

在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 sees that you have defined both a reference and a collection navigation property, so by convention it will configure this as a one-to-many relationship. Based on this, Code First can also determine that Lodging is the dependent end of the relationship (the end with the foreign key) and Destination is the principal end (the end with the primary key). It therefore knows that the table Lodging maps to will need a foreign key pointing back to the primary key of Destination. You saw this played out in Chapter 2, where it created the Destination_DestinationId foreign key field in the Lodgings table.

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

In the rest of this chapter, you will get an understanding of the full set of conventions that Code First has around relationships and how to override those conventions when they don't align with your intent.

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

Relationships in Your Application Logic 
应用程序逻辑中的关系 
Once Code First has worked out the model and its relationships, Entity Framework will treat those relationships just the same as it does with POCOs that are mapped using an EDMX file. All of the rules you learned about working with POCO objects throughout Programming Entity Framework still apply. For example, if you have a foreign key property and a navigation property, Entity Framework will keep them in sync. If you have bidirectional relationships, Entity Framework will keep them in sync as well. At what point Entity Framework synchronizes the values is determined by whether you are leveraging dynamic proxies. Without the proxies, Entity Framework relies on an implicit or explicit call to DetectChanges. With the proxies, the synchronization happens in response to the property value being changed. Typically you do not need to worry about calling DetectChanges because DbContext will take care of calling it for you when you call any of its methods that rely on things being in sync. The Entity Framework team recommends that you only use dynamic proxies if you find a need to; typically this would be around performance tuning. POCO classes without proxies are usually simpler to interact with, as you don't need to be aware of the additional behaviors and nuances that are associated with proxies. 
一旦Code First已经创建了模型与关系,EF框架就会将这些关系视为与使用EDMX文件映射的POCO是类似 的。所有你在使用POCO对象对EF框架编程的方法和规则仍然适用。例如,如何有一个外键属性和一个导航属性关系,EF框架就会保持他们的同步。如果存在双向关系,EF框架也同样会保持他们的同步。EF框架在什么点上同步值取决于您是否在利用动态代理。没有代理,EF框架将会隐式或显示调用DetectChanges。使用代理,同步的响应发生在属性值变更的时候。事实上你不需要关心是否调用DetectChanges因为DbConext将会在你调用任何依赖同步的方法自动调用。EF框架开发团队建议你如果需要只用动态代理;通常这都是围绕着性能调优进行的。没有代理的POCO类通常使交互关系理简化,因为你没必要知道代理相关的附加行为。 

Working with Multiplicity

多重性关系

As you've seen, Code First will create relationships when it sees navigation properties and, optionally, foreign key properties. Details about those navigation properties and foreign keys will help the conventions determine multiplicity of each end. We'll focus on foreign keys a little later in this chapter; for now, let's take a look at relationships where there is no foreign key property defined in your class.

Code First applies a set of rules to work out the multiplicity of each relationship. The rules use the navigation properties you defined in your classes to determine multiplicity. There can either be a pair of navigation properties that point to each other (bidirectional relationship) or a single navigation property (unidirectional relationship):

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

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

• If your classes contain a reference and a collection navigation property, Code First assumes a one-to-many relationship.

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

• Code First will also assume a one-to-many relationship if your classes include a navigation property on only one side of the relationship (i.e., either the collection or the reference, but not both).

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

• If your classes include two collection properties, Code First will use a many-to-many relationship by default.

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

• If your classes include two reference properties, Code First will assume a one-to-one relationship.

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

• In the case of one-to-one relationships, you will need to provide some additional information so that Code First knows which entity is the principal and which is the dependent. You'll see this in action a little later on in this chapter, in the "Working with One-to-One Relationships" on page 84 section. If no foreign key property is defined in your classes, Code First will assume the relationship is optional (i.e., the one end of the relationship is actually zero-or-one as opposed to exactly-one).

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

• In the "Working with Foreign Keys" on page 66 section of this chapter, you will see that when you define a foreign key property in your classes, Code First uses the nullability of that property to determine if the relationship is required or optional.

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

Looking back at the Lodging to Destination relationship that we just revisited, you can see these rules in action. Having a collection and a reference property meant that Code First assumed it was a one-to-many relationship. We can also see that, by convention, Code First has configured it as an optional relationship. But in our scenario it really doesn't make sense to have a Lodging that doesn't belong to a Destination. So let's take a look at how we can make this a required relationship.

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

Configuring Multiplicity with Data Annotations

使用Data Annotations配置多重关系

Most of the multiplicity configuration needs to be done using the Fluent API. But we can use Data Annotations to specify that a relationship is required. This is as simple as placing the Required annotation on the reference property that you want to be required. Modify Lodging by adding the Required annotation to the Destination property (Example 4-3).

大多数多重关系配置都需要使用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; }

}

If you were to run the application so that the database gets recreated with the change you just made, you would see that the Destination_DestinationId column in the Lodgings table no longer allows null values (Figure 4-1). This is because the relationship is now required.

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

Configuring Multiplicity with the Fluent API

使用Fluent API配置多重性关系

Configuring relationships with the Fluent API can look confusing if you haven't taken the time to understand the fundamental ideas. We'll lead you down the path to enlightenment.

When fixing relationships with Data Annotations, you apply annotations directly to the navigation properties. It's very different with the Fluent API, where you are literally configuring the relationship, not a property. In order to do so, you must first identify the relationship. Sometimes it's enough to mention one end, but most often you need to describe the complete relationship.

如果没有花时间去理解基本原理,使用Fluent API配置关系会让人感到迷惑。我们带你对此作些扩展。

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

To identify a relationship, you point to its navigation properties. Regardless of which end you begin with, this is the pattern:

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

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

The multiplicity can be Optional (a property that can have a single instance or be null),Required (a property that must have a single instance), or Many (a property with a collection of a single type).

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

The Has methods are as follows:

Has方法包括如下几个:

• HasOptional

• HasRequired

• HasMany

In most cases you will follow the Has method with one of the following With methods:

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

• WithOptional

• WithRequired

• WithMany

Example 4-4 shows a concrete example using the existing one-to-many relationship between Destination and Lodging. This configuration doesn't really do anything, because it is configuring exactly what Code First detected by convention. Later in this chapter, you will see that this approach is used to identify a relationship so that you can perform further configuration related to foreign keys and cascade delete.

代码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);

 

This identifies a relationship that Destination Has. It has a Many relationship that is defined by its property, Lodgings. And the Lodgings end of the relationship comes along With a relationship (which is Optional) to Destination. Figure 4-2 attempts to help you visualize this relationship the way the model builder sees it.

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

We looked at how to change this to be a required relationship with Data Annotations,so now let's see how to do the same with the Fluent API. Add the configuration shown in Example 4-6 to your DestinationConfiguration class.

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

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

HasMany(d => d.Lodgings)

.WithRequired(l => l.Destination);

This looks very similar to the configuration we saw in Example 4-5, except, instead of calling HasOptional, you are now calling HasRequired. This lets Code First know that you want this one-to-many relationship to be required rather than optional. Run the application again and you will see that the database looks the same as it did in Figure 4-1 when you used Data Annotations to configure the relationship to be required.

这看起来非常类型于代码4-5,只不过调用了HasRequired。这会使Code First你想建立一个必须的一对多关系。运行程序你会看到数据库与图4-1显示的一样,与使用Data Annotations的Required标记产生效果一致。

If you are configuring a one-to-one relationship where both ends are required or both ends are optional, Code First will need some more information from you to work out which end is the principal and which end is the dependent. This area of the Fluent API can get very confusing! The good news is that you probably won't need to use it very often. This topic is covered in detail in "Working with One-to-One Relationships" on page 84. 
如果你想在两端配置全必须的一对一或全可选的一对一关系,Code First会需要更多的信息来获知何为主何为辅。这种Fluent API代码会让人很迷惑!好消息是你可能不需要经常这么做。这一议题将在"1-1关系"中详细讲述。 

Working with Foreign Keys

使用外键

So far we've just looked at relationships where there isn't a foreign key property in your class. For example, Lodging just contains a reference property that points to Destination, but there is no property to store the key value of the Destination it points to. In these cases, we have seen that Code First will introduce a foreign key in the database for you. But now let's look at what happens when we include the foreign key property in the class itself.

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

In the previous section you added some configuration to make the Lodging to Destination relationship required. Go ahead and remove this configuration so that we can observe the Code First conventions in action. With the configuration removed, add a DestinationId property into the Lodging class:

在上一节中,您添加一些配置,使LodgingDestination的关系是必须的。请删除此配置,以例我们可以观察到Code First的约定行为。随着配置中删除,添加到一个DestinationId属性到Lodging类中:

public int DestinationId { get; set; }

Once you have added the foreign key property to the Lodging class, go ahead and run your application. The database will get recreated in response to the change you just made. If you inspect the columns of the Lodgings table, you will notice that Code First has automatically detected that DestinationId is a foreign key for the Lodging to Destination relationship and is no longer generating the Destination_DestinationId foreign key (Figure 4-3).

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

As you might expect by now, Code First has a set or rules it applies to try and locate a foreign key property when it discovers a relationship. The rules are based on the name of the property. The foreign key property will be discovered by convention if it is named [Target Type Key Name], [Target Type Name] + [Target Type Key Name], or [Navigation Property Name] + [Target Type Key Name]. The DestinationId property you added matched the first of these three rules. Name matching is case-insensitive, so you could have named the property DestinationID, DeStInAtIoNiD, or any other variation of casing. If no foreign key is detected, and none is configured, Code First falls back to automatically introducing one in the database.

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

Why Foreign Key Properties? 
为什么要使用外键属性? 
It's common when coding to want to identify a relationship with another class. For example, you may be creating a new Lodging and want to specify which Destination the Lodging is associated with. If the particular destination is in memory, you can set the relationship through the navigation property: 
在通常编码时,要找出一个与其他类的关系。例如,您可能会创建一个新的Lodging,要指定Lodging与哪个Destination相关。如果特定的Destination在内存中,你就可以通过导航属性的设置关系: 
myLodging.Destination=myDestinationInstance; 
However, if the destination is not in memory, this would require you to first execute a query on the database to retrieve that destination so that you can set the property. There are times when you may not have the object in memory, but you do have access to that object's key value. With a foreign key property, you can simply use the key value without depending on having that instance in memory: 
但是,如果Destination不在内存中,这将要求你先执行对数据库的查询,检索Destination,让你可以设置该属性。有时,你可能没有在内存中的对象,但你想访问该对象的键值。带有外键的属性,你可以简单地使用键值而不依赖于内存中的实例: 
myLodging.DestinationId=3; 
Additionally, in the specific case when the Lodging is new and you attach the preexisting Destination instance, there are scenarios where Entity Framework will set the Destination's state to Added even though it already exists in the database. If you are only working with the foreign key, you can avoid this problem. 
此外,在特定情况下,如果Lodging是新建的,您可以附加到原有的Destination实例上,有些情况下,实体框架还要设定Destination的状态为新增,即使已经存在于数据库中。如果你只与外键进行工作,就能避免这个问题。

There's something else interesting that happens when you add the foreign key property.Without the DestinationId foreign key property, Code First convention allowed Lodging.Destination to be optional, meaning you could add a Lodging without a Destination. If you check back to Figure 2-1 in Chapter 2, you'll see that the Destination_DestinationId field in the Lodgings table is nullable. Now with the addition of the DestinationId property, the database field is no longer nullable and you'll find that you can no longer save a Lodging that has neither the Destination nor DestinationId property populated. This is because DestinationId is of type int, which is a value type and cannot be assigned null. If DestinationId was of type Nullable<int>, the relationship would remain optional. By convention, Code First is using the nullability of the foreign key property in your class to determine if the relationship is required or optional.

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

It's Just Easier with Foreign Key Properties Code First allows you define relationships without using foreign key properties in your classes. However, some of the confusing behaviors that developers encounter when working with related data in Entity Framework stems from dependent classes that do not have a foreign key property. The Entity Framework has certain rules that it follows when it checks relationship constraints, performs inserts, etc. When there's no foreign key property to keep track of a required principal (e.g., knowing what the destination is for a particular lodging), it's up to the developer to ensure that you've somehow provided the required information to EF. You can also learn more in Julie's January 2012 Data Points column, "Making Do with Absent Foreign Keys" (http://msdn.com/magazine). 
Code First允许你定义的类中不使用外键属性建立关系,只是使用外键属性更容易建立关系。然而,开发者在与实体框架中的相关数据工作时会遇到的一些混乱的行为源于没有外键属性可进行依赖。EF框架在检查关系约束,执行插入时会遵循一定的规则,当没有外键属性来对一个必须的主实体进行跟踪时(例如,知道一个特定的destination对应特定的lodging)时,就轮到开发者来确保你已经以某种方式提供所需信息给EF。您还可以了解更多"缺少外键下工作"(http://msdn.com/magazine),2012年1月号。

Specifying Unconventionally Named Foreign Keys

指定非规则命名的外键

What happens when you have a foreign key, but it doesn't follow Code First convention?

Let's introduce a new InternetSpecial class that allows us to keep track of special pricing for the various lodgings (Example 4-7). This class has both a navigation property (Accommodation) and a foreign key property (AccommodationId) for the same relationship.

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

我们来引入一个新的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 will need a new property to contain each lodging's special prices:

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

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

Code First can see that Lodging has many InternetSpecials and that InternetSpecials has a Lodging (called Accommodation). Even though there's no DbSet<InternetSpecial>, InternetSpecial is reachable from Lodging and will therefore be included in the model.

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

When you run your application again, it will create the table shown in Figure 4-4. Not only is there an AccommodationId column, which is not a foreign key, but there is also another column there which is a foreign key, Accommodation_LodgingId.

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

You've seen Code First introduce a foreign key in the database before. As early as Chapter 2, you witnessed the Destination_DestinationId field added to the Lodgings table because Code First detected a need for a foreign key. It's done the same here. Thanks to the Accommodation navigation property, Code First detected a relationship to Lodging and created the Accommodation_LodgingId field using its conventional pattern. Code First convention was not able to infer that AccommodationId is meant to be the foreign key. It simply found no properties that matched any of the three patterns that Code First convention uses to detect foreign key properties, and therefore created its own foreign key.

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

Fixing foreign key with Data Annotations

使用Data Annotations修改外键

You can configure foreign key properties using the ForeignKey annotation to clarify your intention to Code First. Adding ForeignKey to the AccommodationId, along with information telling it which navigation property represents the relationship it is a foreign key for, will fix the problem:

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

[ForeignKey("Accommodation")]

public int AccommodationId { get; set; }

public Lodging Accommodation { get; set; }

Alternatively, you can apply the ForeignKey annotation to the navigation property and tell it which property is the foreign key for the relationship:

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

public int AccommodationId { get; set; }

[ForeignKey("AccommodationId")]

public Lodging Accommodation { get; set; }

Which one you use is a matter of personal preference. Either way, you'll end up with the correct foreign key in the database: AccommodationId, as is shown in Figure 4-5.

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

Fixing foreign key with the Fluent API

使用Fluent API来修复外键

The Fluent API doesn't provide a simple way to configure the property as a foreign key. You'll use the relationship API to configure the correct foreign key. And you can't simply configure that piece of the relationship; you'll need to first specify which relationship you want to configure (as you learned how to do earlier in this chapter) and then apply the fix.

To specify the relationship, begin with the InternetSpecial entity. We'll do that directly from the modelBuilder, although you can certainly create an EntityTypeConfiguration class for InternetSpecial.

In this case, we'll be identifying the relationship but not changing the multiplicity that Code First selected by convention. Example 4-8 specifies the existing relationship.

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)

What we want to change, however, is something about the foreign key that is also involved with this relationship. Code First expects the foreign key property to be named LodgingId or one of the other conventional names. So we need to tell it which property truly is the foreign key—AccommodationId. Example 4-9 shows adding the HasForeignKey method to the relationship you specified in Example 4-8.

我们想要改变的,是在这种关系下的外键。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

使用逆导航属性

So far Code First has always been able to work out that the two navigation properties we have defined on each end of a relationship are in fact different ends of the same relationship. It has been able to do this because there has only ever been one possible match. For example, Lodging only contains a single property that refers to Destination (Lodging.Destination); likewise, Destination only contains a single property that references Lodging (Destination.Lodgings).

While it isn't terribly common, you may run into a scenario where there are multiple relationships between entities. In these cases, Code First won't be able to work out which navigation properties match up. You will need to provide some additional configuration.

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

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

For example, what if you kept track of two contacts for each lodging? That would require a PrimaryContact and SecondaryContact property in the Lodging class. Go ahead and add these properties to the Lodging class:

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

public Person PrimaryContact { get; set; }

public Person SecondaryContact { get; set; }

 

Let's also introduce the navigation properties on the other end of the relationship. This will allow you to navigate from a Person to the Lodging instances that they are primary and secondary contact for. Add the following two properties to the Person class:

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

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

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

 

Code First conventions will make the wrong assumptions about these new relationships you have just added. Because there are two sets of navigation properties, Code First is unable to work out how they match up. When Code First can't be sure which navigation properties are the inverse of each other, it will create a separate relationship for each property. Figure 4-6 shows that Code First is creating four relationships based on the four navigation properties you just added.

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

Code First convention can identify bidirectional relationships, but not when there are multiple bidirectional relationships between two entities. The reason that there are extra foreign keys in Figure 4-6 is that Code First was unable to determine which of the two properties in Lodging that return a Person link up to the List<Lodging> properties in the Person class.

You can add configuration (using Data Annotations or the Fluent API) to present this information to the model builder. With Data Annotations, you'll use an annotation called InverseProperty. With the Fluent API, you'll use a combination of the Has/With methods to specify the correct ends of these relationships.

You can place the annotations on either end of the relationship (or both ends if you want). We'll stick them on the navigation properties in the Lodging class (Example 4-10). The InverseProperty Data Annotation needs the name of the corresponding navigation property in the related class as its parameter.

Code First默认规则可以识别双向关系,但不能识别在两个实体中多个双向关系。原因是会多如图4-6所示的多个外键,使得!CF无法确定哪个在Lodging的属性连接到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; }

With the Fluent API, you need to use the Has/With pattern that you learned about earlier to identify the ends of each relationship. The first configuration in Example 4-11 describes the relationship with Lodging.PrimaryContact on one end and Person.Primary ContactFor on the other. The second configuration is for the relationship between SecondaryContact and SecondaryContactFor.

使用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);

Working with Cascade Delete

使用级连删除

Cascade delete allows dependent data to be automatically deleted when the principal record is deleted. If you delete a Destination, for example, the related Lodgings will also be deleted automatically. Entity Framework supports cascade delete behavior for in memory data as well as in the database. As discussed in Chapter 19 of the second edition of Programming Entity Framework, it is recommended that you implement cascade delete on entities in the model if their mapped database objects also have cascade delete defined.

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

By convention, Code First switches on cascade delete for required relationships. When a cascade delete is defined, Code First will also configure a cascade delete in the database that it creates. Earlier in this chapter we looked at making the Lodging to Destination relationship required. In other words, a Lodging cannot exist without a Destination. Therefore, if a Destination is deleted, any related Lodgings (that are in memory and being change-tracked by the context) will also be deleted. When SaveChanges is called, the database will delete any related rows that remain in the Lodgings table, using its cascade delete behavior.

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

Looking at the database, you can see that Code First carried through the cascade delete and set up a constraint on the relationship in the database. Notice the Delete Rule in Figure 4-7 is set to Cascade.

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

Example 4-12 shows a new method called DeleteDestinationInMemoryAndDbCascade, which we'll use to demonstrate the in-memory and database cascade delete.

代码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();

}

}

 

The code uses a context to insert a new Destination with a couple of Lodgings. It then saves these Lodgings to the database and records the primary of the new Destination. In a separate context, the code then retrieves the Destination and its related Lodgings, and then uses the Remove method to mark the Destination instance as Deleted. We use Console.WriteLine to inspect the state of one of the related Lodging instances that are in memory. We'll do this using the Entry method of DbContext. The Entry method gives us access to the information that EF has about the state of a given object. Next, the call to SaveChanges persists the deletions to the database.

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

After calling Remove on the Destination, the state of a Lodging is displayed in the console window. It is Deleted also even though we did not explicitly remove any of the Lodgings. That's because Entity Framework used client-side cascade deleting to delete the dependent Lodgings when the code explicitly deleted (Removed) the destination.

Next, when SaveChanges is called, Entity Framework sent three DELETE commands to the database, as shown in Figure 4-8. The first two are to delete the related Lodging instances that were in memory and the third to delete the Destination.

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

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

Now let's change the method. We'll remove the eager loading (Include) that pulled the Lodging data into memory along with Destination. We'll also remove all of the related code that mentions the Lodgings. Since there are no Lodgings in memory, there will be no client-side cascade delete, but the database should clean up any orphaned Lodgings because of the cascade delete defined in the database (Figure 4-7). The revised method is listed in Example 4-13.

现在我们来改变一下方法。我们将要要随同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);

}

}

When run, the only command sent to the database is one to delete the destination. The database cascade delete will delete the related lodgings in response. When querying for the Lodgings at the end, since the database deleted the lodgings, the query will return no results and the lodgings variable will be an empty list.

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

Turning On or Off Client-Side Cascade Delete with Fluent Configurations

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

You might be working with an existing database that does not use cascade delete or you may have a policy of being explicit about data removal and not letting it happen automatically in the database. If the relationship from Lodging to Destination is optional, this is not a problem, since by convention, Code First won't use cascade delete with an optional relationship. But you may want a required relationship in your classes without leveraging cascade delete.

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

You may want to get an error if the user of your application tries to delete a Destination and hasn't explicitly deleted or reassigned the Lodging instances assigned to it. For the scenarios where you want a required relationship but no cascade delete, you can explicitly override the convention and configure cascade delete behavior with the Fluent API. This is not supported with Data Annotations.

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

Keep in mind that if you set the model up this way, your application code will be responsible for deleting or reassigning dependent data when necessary.

The Fluent API method to use is called WillCascadeOnDelete and takes a Boolean as a parameter. This configuration is applied to a relationship, which means that you first need to specify the relationship using a Has/With pairing and then call WillCascadeOnDelete.

请记住,如果你成立这样的模型,应用程序代码将会实现负责任地删除或在必要时重新分配相关的数据。

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

Working within the LodgingConfiguration class, the relationship is defined as:

在LodgingConfiguration类中,关系定义为:

HasRequired(l=>l.Destination)

.WithMany(d=>d.Lodgings)

From there, you'll find three possible configurations to add. WillCascadeOnDelete is

one of them, as you can see in Figure 4-9.

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

Now you can set WillCascadeOnDelete to false for this relationship:

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

HasRequired(l=>l.Destination)

.WithMany(d=>d.Lodgings)

.WillCascadeOnDelete(false)

This will also mean that the database schema that Code First generates will not include the cascade delete. The Delete Rule that was Cascade in Figure 4-7 would become No Action.

这也意味着Code First生成的数据库架构将不会包含级联删除。如图4-7所示的级联删除规则将不会出现。

In the scenario where the relationship is required, you'll need to be aware of logic that will create a conflict, for example, the current required relationship between Lodging and Destination that requires that a Lodging instance have a Destination or a DestinationId. If you have a Lodging that is being change-tracked and you delete its related Destination, this will cause Lodging.Destination to become null. When SaveChanges is called, Entity Framework will attempt to synchronize Lodging.DestinationId, setting it to null. But that's not possible and an exception will be thrown with the following detailed message:

在关系为必须的场景下,你应该意识到这种逻辑会创建一个冲突,例如,目前在LodgingDestination的必须关系中,需要一个Lodging实例有一个estination或一个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.

系不能改变,因为一个或多个外键的属性非空。当一个变化是一个关系,相关的外键的属性设置为空值。如果外键不支持空值,必须定义一个新的关系,外键的属性必须指派另一个非空值,必须删除无关的对象。

The overall message here is that you have control over the cascade delete setting, but you will be responsible for avoiding or resolving possible validation conflicts caused by not having a cascade delete present.

这里的整体信息是,你必须控制级联删除设置,并且为避免或解决验证没有级联删除可能引起的冲突负责。

Setting Cascade Delete Off in Scenarios That Are Not Supported by the Database

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

Some databases (including SQL Server) don't support multiple relationships that specify cascade delete pointing to the same table. Because Code First configures required relationships to have cascade delete, this results in an error if you have two required relationships to the same entity. You can use WillCascadeOnDelete(false) to turn off the cascade delete setting on one or more of the relationships. Example 4-14 shows an example of the exception message from SQL Server if you don't configure this correctly.

许可数据库(包括SQL Server)不支持指定级联删除指向到同一个表的多重关系。由于Code First配置的必须关系包括级联删除,如果有两个必须关系指向同一个实体就会出现错误。你可以使用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.

 

Consider Performance Implications of Client-Side Cascade Delete 
考虑客户端级联删除对性能的影响 
Whether you are using Code First, Database First, or Model First, you should keep in mind the performance implications of cascade delete. If you delete a principal, or "parent," without having the related object(s) in memory, the database will take care of the cascade delete. If you pull all of the related objects into memory and let the client-side cascade delete affect those related objects, then call SaveChanges, SaveChanges will send DELETE commands to the database for each of those related objects. There may be cases where those related objects are in memory and you do indeed want them to be deleted. But if you don't need them in memory and can rely on the database to do the cascade delete, you should consider avoiding pulling them into memory. 
不管你是使用Code First,还是用Database First, Model First,你都要记住级联删除的性能影响。如果你删除了一个上级主题,或者"父"主题,如果内存中没有相关对象,数据库本身就会实施级联删除。如果你将所有相关对象调入内存,让客户端级联删除影响这些对象,在调用SaveChanges方法时,saveChange将会发送针对这些相关对象的Delete命令到数据库。可能有情况下,这些相关对象在内存中,你真的希望他们能够被删除。但是,如果你不需要在内存中,并可以依靠的数据库做级联删除,你应该考虑避免他们放入内存。

Exploring Many-to-Many Relationships

探索多对多关系

Entity Framework supports many-to-many relationships. Let's see how Code First responds to a many-to-many relationship between two classes when generating a database.

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

If you've had many-to-many relationships when using the database-first strategy, you may be familiar with the fact that Entity Framework can create many-to-many mappings when the database join table contains only the primary keys of the related entities. This mapping rule is the same for Code First.

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

Let's add a new Activity class to the model. Activity, shown in Example 4-15, will be related to the Trip class. A Trip can have a number of Activities scheduled and an Activity can be scheduled for a variety of trips. Therefore Trip and Activity will have a many-to-many relationship.

我们添加一个新的类: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; }

}

}

There's a List<Trip> in the Activity class. Let's also add a List<Activity> to the Trip class for the other end of the many-to-many relationship:

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

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

When you run the application again, Code First will recreate the database because of the model changes. Code First convention will recognize the many-to-many relationship and build a join table in the database with the appropriate keys of the tables it's joining. The keys are both primary keys of the join table and foreign keys pointing to the joined tables, as shown in Figure 4-10.

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

Notice that Code First convention created the table name by combining the names of the classes it's joining and then pluralizing the result. It also used the same pattern we've seen earlier for creating the foreign key names. In Chapter 5, which focuses on table and column mappings, you'll learn how to specify the table name and column names of the join table with configurations.

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

Once the many-to-many relationship exists, it behaves in just the same way that many-to-many relationships have worked in Entity Framework since the first version. You can query, add, and remove related objects by using the class properties. In the background, Entity Framework will use its knowledge of how your classes map to the database to create select, insert, update, and delete commands that incorporate the join table.

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

For example, the following query looks for a single trip and eager loads the related Activities:

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

var tripWithActivities = context.Trips

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

The query is written against the classes with no need to be concerned about how the trip and its activities are joined in the database. Entity Framework uses its knowledge of the mappings to work out the SQL that performs the join and returns a graph that includes all of the activities that are bound to the first trip. This may not be exactly how you would construct the SQL, but remember that Entity Framework constructs the store SQL based on a pattern that can be used generically regardless of the structure of your classes or the schema of the database.

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

The result is a graph of the trip and its activities. Figure 4-11 shows the Trip in a debug window. You can see it has two Activities that were pulled back from the database along with the Trip.

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

Entity Framework took care of the joins to get across the join table without you having to be aware of its presence. In the same way, any time you do inserts, updates, or deletes within this many-to-many relationship, Entity Framework will work out the proper SQL for the join without you having to worry about it in your code.

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

Working with Relationships that Have Unidirectional Navigation

使用单边导航的关系

So far we have looked at relationships where a navigation property is defined in both classes that are involved in the relationship. However, this isn't a requirement when working with the Entity Framework.

In your domain, it may be common place to navigate from a Destination to its associated Lodging options, but a rarity to navigate from a Lodging to its Destination. Let's go ahead and remove the Destination property from the Lodging class (Example 4-16).

到目前为止我们已经观察了导航属性已经定义在两个类中的关系。但是,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; }

}

Entity Framework is perfectly happy with this; it has a very clear relationship defined from Lodging to Destination with the Lodgings property in the Destination class. This still causes the model builder to look for a foreign key in the Lodging class and Lodging.DestinationId satisfies the convention.

Now let's go one step further and remove the foreign key property from the Lodging class, as shown in Example 4-17.

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

}

Remember the Code First convention that will introduce a foreign key if you don't define one in your class? That same convention still works when only one navigation property is defined in the relationship. Destination still has a property that defines its relationship to Lodging. In Figure 4-13 you can see that a Destination_DestinationId column is added into the Lodgings table. You might recall that the convention for naming the foreign key column was [Navigation Property Name] + [Primary Key Name]. But we no longer have a navigation property on Lodging. If no navigation property is defined on the dependent entity, Code First will use [Principal Type Name] + [Primary KeyName]. In this case, that happens to equate to the same name.

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

What if we tried to just define a foreign key and no navigation properties in either class? Entity Framework itself supports this scenario, but Code First does not. Code First requires at least one navigation property to create a relationship. If you remove both navigation properties, Code First will just treat the foreign key property as any other property in the class and will not create a foreign key constraint in the database. 
那么如果我们试图在另一个类中只定义外键而没有导航属性呢,EF框架本身支持这种情况,但Code First不支持。Code First需要至少一个导航属性来创建关系。如果你移除了两边的导航属性,Code First将只将外键属性作为任何类中的其他属性而不会在数据库中创建约束。 

Now let's change the foreign key property to something that won't get detected by convention. Let's use LocationId instead of DestinationId, as shown in Example 4-18. Remember that we have no navigation property; it's still commented out.

现在我将外键属性调整为默认规则无法检测到的情况。我们用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; }

}

Thanks to Destination.Lodgings, Code First knows about the relationship between the two classes. But it cannot find a conventional foreign key. We've been down this road before. All we had to do was add some configuration to identify the foreign key.

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

In previous examples, we placed the ForeignKey annotation on the navigation property in the dependent class or we placed it on the foreign key property and told it which navigation property it belonged to. But we no longer have a navigation property in the dependent class. Fortunately, we can just place the data annotation on the navigation property we do have (Destination.Lodgings). Code First knows that Lodging is the dependent in the relationship, so it will search in that class for the foreign key:

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

[ForeignKey("LocationId")]

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

The Fluent API also caters to relationships that only have one navigation property. The Has part of the configuration must specify a navigation property, but the With part can be left empty if there is no inverse navigation property. Once you have specified the Has and With sections, you can call the HasForeignKey method you used earlier:

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

modelBuilder.Entity<Destination>()

.HasMany(d => d.Lodgings)

.WithRequired()

.HasForeignKey(l => l.LocationId);

While a unidirectional relationship may make sense in some scenarios, we want to be able to navigate from a Lodging to its Destination. Go ahead and revert the changes to the Lodging class. Uncomment the Destination property and rename the foreign key property back to DestinationId, as shown in Example 4-19. You'll also need to remove the ForeignKey annotation from Destination.Lodging and remove the above Fluent API configuration if you added it.

在我们需要创建单边关系时,很多情况下我们想要从Lodgin导航回相应的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; }

}

Working with One-to-One Relationships

使用一对一关系

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

}

}

Let's also add a Photo property to the Person class, so that we can navigate both directions:

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

public PersonPhoto Photo { get; set; }

Remember that Code First can't determine which class is the dependent in these situations. When it attempts to build the model, an exception is thrown, telling you that it needs more information:

记住在这种情况下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或DA进行显示配置。 

This problem is most easily solved by using a ForeignKey annotation on the dependent class to identify that it contains the foreign key. When configuring one-to-one relationships, Entity Framework requires that the primary key of the dependent also be the foreign key. In our case PersonPhoto is the dependent and its key, PersonPhoto.PersonId, should also be the foreign key. Go ahead and add in the ForeignKey annotation to the PersonPhoto.PersonId property, as shown in Example 4-21. Remember to specify the navigation property for the relationship when adding the ForeignKey annotation.

这个问题可以很容易地使用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; }

}

Running the application again will successfully create the new database table, although you'll see that Entity Framework didn't deal well with pluralizing the word "Photo." We'll clean that up in Chapter 5, when you learn how to specify table names. More importantly, notice that PersonId is now both a PK and an FK. And if you look at the PersonPhoto_PhotoOf foreign key constraint details, you can see that it shows the People.PersonId is the primary table/column in the relationship and PersonPhotoes.PersonId is the foreign key table/column (Figure 4-14). This matches our intent.

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

Earlier in this chapter, we also saw that you could place the ForeignKey annotation on the navigation property and specify the name of the foreign key property (in our case, that is PersonId). Since both classes contain a PersonId property, Code First still won't be able to work out which class contains the foreign key. So you can't employ the configuration in that way for this scenario.

Of course, there is also a way to configure this in the Fluent API. Let's assume for the moment that the relationship is one-to-zero-or-one, meaning a PersonPhoto must have a Person but a Person isn't required to have a PersonPhoto. We can use the HasRequired and WithOptional combination to specify this:

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

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

modelBuilder.Entity<PersonPhoto>()

.HasRequired(p => p.PhotoOf)

.WithOptional(p => p.Photo);

That's actually enough for Code First to work out that PersonPhoto is the dependent. Based on the multiplicity we specified, it only makes sense for Person to be the principal and PersonPhoto to be the dependent, since a Person can exist without a PersonPhoto but a PersonPhoto must have a Person.

Notice that you didn't need to use HasForeignKey to specify that PersonPhoto.PersonId is the foreign key. This is because of Entity Framework's requirement that the primary key of the dependent be used as the foreign key. Since there is no choice, Code First will just infer this for you. In fact, the Fluent API won't let you use HasForeignKey. In IntelliSense, the method simply isn't available after combining HasRequired and WithOptional.

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

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

Configuring One-to-One Relationships When Both Ends Are Required

当两端都是必须项是配置一对一关系

Now let's tell Code First that a Person must have a PersonPhoto (i.e., it's required). With Data Annotations, you can use the same Required data annotation that we used earlier on Destination.Name and Lodging.Name. You can use Required on any type of property,not just primitive types:

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

[Required]

public PersonPhoto Photo { get; set; }

Now update the Main method to call the InsertPerson method you defined back in Chapter 3 and run the application again. An exception will be thrown when SaveChanges is called. In the exception, Entity Framework's Validation API reports that the validation for the required PersonPhoto failed.

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

Ensuring that the sample code honors the required Photo

确保使用代码满足必须要求

If you want to leave the Photo property as Required and avoid the validation errors, you can modify the InsertPerson and UpdatePerson methods so that they add data into the Photo field. For the sake of keeping the code simple, we'll just stuff a single byte into the Photo's byte array rather than worrying about supplying an actual photo.

In the InsertPerson method, modify the line of code that instantiates a new Person object to add the Photo property, as shown in Example 4-22.

如果你想让Photo属性为必须项避免验证错误,你可以修改InsertPerson和UpdatePerson方法以便使用它们可针数据添加到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 } }

};

In the UpdatePerson method, we'll add some code to ensure that any Person data you've already added before we created the Photo class gets a Photo at the same time that you update. Modify the UpdatePerson method as shown in Example 4-23 so that it allocates a new PersonPhoto when it tries to update a person without a photo

在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();

}

}

The updated method will use Include to also retrieve the Person's Photo when fetching the data from the database. We then check if the Person has a Photo and add a new one if they do not. Now the Photo requirement in the Person class will be fulfilled any time you execute the InsertPerson and UpdatePerson methods.

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

Configuring one-to-one with the Fluent API

使用Fluent API配置一对一关系

Not surprisingly, you can also configure the same relationship with the Fluent API. But you'll need to let Code First know which class is the principal and which is the dependent. If both ends are required, this can't simply be implied from the multiplicity.

You might expect to call HasRequired followed by WithRequired. However, if you start with HasRequired, you will have the additional options of WithRequiredPrincipal and WithRequiredDependent in the place of WithRequired. These methods take into account the entity that you are configuring; that is, the entity that you selected in model Builder.Entity or the entity that your EntityTypeConfiguration class is for. Selecting WithRequiredPrincipal will make the entity that you are configuring the principal, meaning it contains the primary key of the relationship. Selecting WithRequiredDependent will make the entity that you are configuring the dependent, meaning it will have the foreign key of the relationship.

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

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

Assuming you are configuring PersonPhoto, which you want to be the dependent, you would use the following configuration:

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

modelBuilder.Entity<PersonPhoto>()

.HasRequired(p => p.PhotoOf)

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

Configuring a one-to-one relationship where both ends are optional works exactly the same, except you start with HasOptional and select either WithOptionalPrincipal or WithOptionalDependent.

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

Summary

小结

In this chapter, you've seen that Code First has a lot of intelligence about relationships. Code First conventions are able to discover relationships of any multiplicity with or without a provided foreign key. But there are many scenarios where your intentions don't coincide with Code First conventions. You've learned many ways to "fix" the model by configuring with Data Annotations and the Fluent API. You should have a good understanding of how to work with relationships in the Fluent API based on its Has/With pattern.

In the next chapter, we'll look at another set of mappings in Code First that are all about how your classes map to the database, including how to map a variety of inheritance hierarchies.

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

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

 

posted @ 2012-02-22 17:20  ido  阅读(11432)  评论(1编辑  收藏  举报