對關系使用默認規則與配置
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:
在上一節中,您添加一些配置,使Lodging與Destination的關系是必須的。請刪除此配置,以例我們可以觀察到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是一個外鍵,對應於Lodging與Destination的關系,不再產生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相匹配。名稱匹配是區分大小寫的,所以你可以有一個名為DestinationID,DeStInAtIoNiD,或任何其他變化的屬性,(將不會被匹配,譯者注)。如果沒有檢測到外鍵,也沒有配置,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是可選的,這意味着你可以添加沒有Destination的Lodging。如果回到第2章中的圖2-1,你會看到,在Lodgings表中Destination_DestinationId字段可為空。現在DestinationId屬性加入,數據庫中的字段不再是可空的,你會發現,你不再可以保存沒有Destination,或沒有DestinationId屬性填充的Lodging數據。這是因為DestinationId是int類型,這是一個值類型,不能分配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.
你可能會在現有的數據庫上工作,不使用級聯刪除或者你可能有一個規則必須顯示刪除數據,不允許在數據庫中自動刪除。如果從Lodging到Destination之間的關系是可選的,這不是一個問題,因為按照默認規則,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:
在關系為必須的場景下,你應該意識到這種邏輯會創建一個沖突,例如,目前在Lodging和Destination的必須關系中,需要一個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的另一套映射,就是類如何映射到數據庫,包括如何映射到各種繼承架構等。