Code First :使用Entity. Framework編程(4)


第4章

對關系使用默認規則與配置

在第3章,你已經掌握了默認規則與配置對屬性以及其在數據庫映射的字段的影響。在本章,我們把焦點放在類之間的關系上面。這包括類在內存如何關聯,還有數據庫中的外鍵維持等。你將了解控制多重性關系,無論是否是必須的,還將學習級聯刪除操作。你會看到默認行為以及如何使用Data Annnotations和Fluent API來控制關系。

你會看到很多只能使用Fluent API而不能使用Data Annotations的情況。上一章我們介紹過"映射到非Unicode數據庫類型"就只能在Fluent API中找到。在前幾章你已經看到了幾個有關默認關系的例子,如代碼4-1,就是通過建立類型為List<Lodging>的Lodging屬性與炻Destination建立了聯系。

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

public class Destination

{

public int DestinationId { get; set; }

public string Name { get; set; }

public string Country { get; set; }

public string Description { get; set; }

public byte[] Photo { get; set; }

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

}

在Lodging類(代碼4-2)也有一個Destination屬性代表單個Destination實例。

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

public class Lodging

{

public int LodgingId { get; set; }

public string Name { get; set; }

public string Owner { get; set; }

public bool IsResort { get; set; }

public decimal MilesFromNearestAirport { get; set; }

public Destination Destination { get; set; }

}

 

Code First觀察到您既定義了一個引用又有一個集合導航屬性,因此引用默認規則將其配置為一對多關系。基於此,Code First可以確定Lodging(外鍵)與Destination(主鍵)具有依賴關系。因此獲表Lodging需要要一個外鍵映射到Destination的主鍵。在第2章你已經看到,在Lodgings表中確實建立了Destination_DestinationId外鍵字段。

本章將全面解析Code First在處理關系的默認規則以及如何按我們的意圖覆寫這些規則。

應用程序邏輯中的關系 
一旦Code First已經創建了模型與關系,EF框架就會將這些關系視為與使用EDMX文件映射的POCO是類似的。所有你在使用POCO對象對EF框架編程的方法和規則仍然適用。例如,如何有一個外鍵屬性和一個導航屬性關系,EF框架就會保持他們的同步。如果存在雙向關系,EF框架也同樣會保持他們的同步。EF框架在什么點上同步值取決於您是否在利用動態代理。沒有代理,EF框架將會隱式或顯示調用DetectChanges。使用代理,同步的響應發生在屬性值變更的時候。事實上你不需要關心是否調用DetectChanges因為DbConext將會在你調用任何依賴同步的方法自動調用。EF框架開發團隊建議你如果需要只用動態代理;通常這都是圍繞着性能調優進行的。沒有代理的POCO類通常使交互關系理簡化,因為你沒必要知道代理相關的附加行為。 

多重性關系

如前所述,Code First在看到導航屬性和可選的外鍵屬性時將創建關系。有關導航屬性和外鍵屬性的細節將幫助我們來確定多重關系的每一端。本章對外鍵將關注更多一點;現在我們來看看在類中沒有外鍵屬性定義的情況。

Code First在處理多重性關系時應用了一系列規則。規則使用導航屬性確定多重性關系。即可以是一對導航屬性互相指定(雙向關系),也可以是單個導航屬性(單向關系)。

•如果你的類中包含一個引用和一個集合導航屬性,Code First視為一對多關系;

• 如果你的類中僅在單邊包含導航屬性(即要么是集合要么是引用,只有一種),Code First也將其視為一對多關系;

• 如果你的類包含兩個集合屬性,Code First默認會使用多對多關系;

•如果你的類包含兩個引用屬性,Code First會視為一對一關系;

•在一對一關系中,你需要提供附加信息以使Code First獲知何為主何為輔。本章后面會在"一對一關系"中提到。如果沒有在類中定義外鍵屬性,Code First將設定關系為可選(即一端的關系實際是零對一或恰好相反,零指的是可空---譯者注)。

•在本章的"外鍵"小節,你會看到當在類中定義外鍵屬性,Code First會使用屬性的可空性來確定關系是必須的還是可選的。

回顧我們剛剛重溫的Lodging與destination的關系,你會看到上述規則。由於有集合和引用屬性,Code First就將其視為一對多關系。同時也看到,通過默認規則,Code First將經將其配置為可選關系(optional)。但是在我們的場景里,確實沒有想讓一個Lodging(住所)不從屬於一個Destination(目的地)。因此我們來看看如何確保這種關系是必須的。

使用Data Annotations配置多重關系

大多數多重關系配置都需要使用Fluent API。但是我可以使用Data Annotations來指定一些關系是必須的。只需要簡單地將Required標記放在你需要定義為必須項的引用屬性上。修改Lodging類的代碼將Required特性標記放在Destination屬性上(代碼4-3):

Example 4-3. Required annotation added to Destination property

public class Lodging

{

public int LodgingId { get; set; }

public string Name { get; set; }

public string Owner { get; set; }

public bool IsResort { get; set; }

public decimal MilesFromNearestAirport { get; set; }

[Required]

public Destination Destination { get; set; }

}

運行程序,數據庫重新創建,你會看到Lodgings表中Destination_DestinationId不再允許空值(圖4-1)。這是因為關系現在是必須的(Required)。

使用Fluent API配置多重性關系

如果沒有花時間去理解基本原理,使用Fluent API配置關系會讓人感到迷惑。

當使用Data Annotations修改關系時,可以將特性直接放在了導航屬性上。這與Fluent API不同,Fluent API並不直接在屬性上配置關系。為了達到相同的目的,必須先確定關系。有時在一端就足夠,但更多的需要對全部關系進行描述。

為了確定關系,你必須指明導航屬性。不管從哪端開始,都要使用這樣的代碼模板:

 

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

 

多重性關系可以是Optional(一個屬性可擁有一個單個實例或沒有),Required(一個屬性必須擁有一個單個實例)或很多的(一個屬性可以擁有一個集合或一個單個實例)。

Has方法包括如下幾個:

• HasOptional

• HasRequired

• HasMany

在多數情況還需要在Has方法后面跟隨如下With方法之一:

• WithOptional

• WithRequired

• WithMany

代碼4-4顯示了一個 使用現有的Destination和Lodging之間的一對多關系的實例。這一配置並非真的做任何事,因為這會被Code First通過默認規則同樣進行配置。本章后面會看到識別這種關系然后作進一步的配置,實現外鍵關系和級聯刪除功能。

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

 

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

 

 

這一代碼確定Destination的Has關系。有很多由Lodgings的屬性所定義的關系。Lodgings端到Destination的關系是可選的。圖4-2嘗試幫你觀察這種關系建立的過程。

我們來看看如何使用Fluent API建立Required關系。在DestinationConfiguration添加代碼4-6:

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

 

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

 

這看起來非常類似於代碼4-5,只不過調用了HasRequired來取代HasOptional。這會使Code First知曉你想建立一個必須的(Required)一對多關系。運行程序你會看到數據庫與圖4-1顯示的一樣,與使用Data Annotations的Required標記產生效果一致。如果你想在兩端配置全必須的一對一或全可選的一對一關系,Code First會需要更多的信息來獲知何為主何為輔。這種Fluent API代碼會讓人很迷惑!好消息是你可能不需要經常這么做。這一議題將在"1-1關系"中詳細講述。 

 

使用外鍵

到目前為止,我們只是看了在類中沒有外鍵屬性的關系。例如,Lodging只包含一個引用屬性到Destination,但沒有屬性來存儲它指向Destination的鍵值。在這種情況下,我們已經看到,Code First會為你的數據庫引入外鍵。我們來看看在如果在類本身引入鍵屬性時會發生什么。

在上一節中,通過添加一些配置,建立了LodgingDestination的Required關系。請刪除此配置,以使我們可以觀察到Code First的約定行為。配置刪除后,添加到一個DestinationId屬性到Lodging類中:

public int DestinationId { get; set; }

一旦你添加外鍵屬性到Lodging類,繼續運行您的應用程序。該數據庫將回應你剛才的改變重新創建。如果您檢查Lodgings表列,你會發現,Code First自動檢測DestinationId是一個外鍵,對應於LodgingDestination的關系,不再產生Destination_DestinationId外鍵(圖4-3)。

正如您現在可能期望的,Code First有一個設置或規則得到了應用,結果就是發現了一個關系嘗試並找到一個外鍵屬性。規則使用的是屬性的名稱。按照默認規則,一旦發現外鍵屬性,就被命名為“[目標類型的鍵名][目標類型名稱]+[目標類型鍵名稱]”,或“[導航屬性名稱]+[目標類型鍵名稱]”的形式。前面提到的三個規則中的第一個與您添加的屬性DestinationId相匹配。名稱匹配是區分大小寫的,所以你可以有一個名為DestinationIDDeStInAtIoNiD,或任何其他變化的屬性,(將不會被匹配,譯者注)。如果沒有檢測到外鍵,也沒有配置,Code First會自動在數據庫中設置一個。

為什么要使用外鍵屬性? 
在編寫代碼時,要找出一個與其他類的關系。例如,您可能會創建一個新的Lodging,要指定Lodging與哪個Destination相關。如果特定的Destination在內存中,你就可以通過導航屬性的設置關系: 
myLodging.Destination=myDestinationInstance; 
但是,如果Destination不在內存中,這將要求你先執行對數據庫的查詢,檢索Destination,讓你可以設置該屬性。有時,你可能沒有在內存中的對象,但你想訪問該對象的鍵值。
帶有外鍵的屬性,你可以簡單地使用鍵值而不依賴於內存中的實例: 
myLodging.DestinationId=3; 
此外,在特定情況下,如果Lodging是新建的,您可以附加到原有的Destination實例上,有些情況下,實體框架還要設定Destination的狀態為新增,即使所需的Destination實例已經
存在於數據庫中。如果你只與外鍵進行工作,就能避免這個問題。

還有一些有趣的現象會在添加外鍵屬性時會發生。沒有 DestinationId外鍵屬性時,Code First的約定規則允許Lodging.Destination是Optional,這意味着你可以添加沒有DestinationLodging。如果回到第2章中的圖2-1,你會看到,在Lodgings表中Destination_DestinationId字段為可空類型。現在DestinationId屬性加入,數據庫中的字段不再是可空的,你會發現,你不再可以保存沒有Destination,或沒有DestinationId屬性填充的Lodging數據。這是因為DestinationIdint類型,這是一個值類型,不能分配null值。如果DestinationId類型是Nullable<int>的,這種關系將保持Optional狀態。事實上,Code First默認約定就是根據類中外鍵屬性的可空性,來確定是否關系是Required還是Optional的。

Code First允許你定義的類中不使用外鍵屬性建立關系,只是使用外鍵屬性更容易建立關系。然而,由於沒有外鍵屬性可以依賴,開發者在與實體框架中的相關數據工作時會遇到一些混亂的行為。如果有外鍵屬性,EF框架會在執行插入時檢查關系約束,可以避免插入無效數據。沒有外鍵屬性來對一個要求為Required的主實體進行跟蹤時(例如,要求一個特定的destination必須對應特定的lodging)時,就需要開發者自己來確保以某種方式提供所需信息給EF(很顯然,這樣更麻煩---譯者注)。您還可以了解更多有關"缺少外鍵下工作"的信息,見2012年1月號(http://msdn.com/magazine) 。

指定非規則命名的外鍵

如果有一個不遵循規則的外鍵會怎么樣呢?

我們來引入一個新的InternetSpecial類,來跟蹤一些各種lodging的特定價格(代碼4-7)。這個類即有導航屬性(Accommodation),又有外鍵屬性(AccommodationId),都是為同一關系設立的。

Example 4-7. The new InternetSpecial class

using System;

namespace Model

{

public class InternetSpecial

{

public int InternetSpecialId { get; set; }

public int Nights { get; set; }

public decimal CostUSD { get; set; }

public DateTime FromDate { get; set; }

public DateTime ToDate { get; set; }

public int AccommodationId { get; set; }

public Lodging Accommodation { get; set; }

}

}

在Lodging中需要一個新的屬性來包含每個logding的特定報價。

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

Code First看到Lodging有很多InternetSpecials,而InternetSpecials又有一個Lodging(稱之為Accommodation).盡管沒有設置DbSet<InternetSpecial>, InternetSpecial也可以通過Lodging而包含在模型里。

再次運行程序,將會創建如圖4的表。不僅有不是外鍵的AccommodataionId列,也新增了一個外鍵列,Accommodation_LodgingId。

你會看到Code First引入一個外鍵。Code First根據Accommodation導航屬性,檢測到了一個對應對Lodging的關系然后使用默認規則創建了Accommodation_LodgingId字段。默認規則無法將AccommodationId推斷為外鍵,因為Code First檢查了默認規則對外鍵屬性名稱的三個要求沒有在類中找到匹配項,就創建了自己的外鍵。

使用Data Annotations修改外鍵

你可以使用 Data Annotations 的配置外鍵特性ForeignKey來聲明外鍵屬性。在AccommodtaionId上添加ForeignKey特性告知Code First哪個導航屬性是外鍵,來修復這個問題。

[ForeignKey("Accommodation")]

public int AccommodationId { get; set; }

public Lodging Accommodation { get; set; }

你也可以將ForeignKey特性放在導航屬性上來通知哪個屬性是關系的外鍵。

public int AccommodationId { get; set; }

[ForeignKey("AccommodationId")]

public Lodging Accommodation { get; set; }

兩種寫法都可以。與此同時,你獲得的正確的的數據庫外鍵:AccommodtationId,如圖4-5所示。

使用Fluent API來修改外鍵

 

Fluent API並沒有提供配置屬性作為外鍵的簡單方法。你要使用專門的關系API來配置正確的外鍵。而且你不能簡單地配置關系的片斷,你需要首先指定你想配置的關系類型(前面已經提到)然后才能應用修改。

為了指定關系,需要從IneternetSpecial實體開始,我們直接在modelBuilder中進行配置,當然也你可以在EntityTypeConfiguration類中為InternetSpecial創建一個實例。

在這種情況下,我們先要設置關系而不打破Code First建立的默認關系。代碼4-8指出了這種關系:

Example 4-8. Identifying the relationship to be configured

modelBuilder.Entity<InternetSpecial>()

.HasRequired(s => s.Accommodation)

.WithMany(l => l.InternetSpecials)

我們想要改變的,是在這種關系下的外鍵。Code First期待外鍵屬性命名為LodgingId或者是其他的默認名稱。因此我們需要告訴AccommodationId 屬性才是真正的外鍵:.代碼4-9添加了HasForeignKey方法來為關系指定外鍵:

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

modelBuilder.Entity<InternetSpecial>()

.HasRequired(s => s.Accommodation)

.WithMany(l => l.InternetSpecials)

.HasForeignKey(s => s.AccommodationId);

 

效果與圖4-5一致。

Working with Inverse Navigation Properties

使用逆導航屬性

Code First到目前為止一直能夠解析我們定義的兩個導航屬性,雖然它們處於不同端,實際上是同一關系。它之所以能做到這一點,因為兩端至少有一個可能的匹配關系。例如,Lodging只包含一個單一的屬性,指向目的地(Lodging.Destination;同樣地,目的地Destination只包含一個屬性引用住所(Destination.Lodgings)。

雖然並不十分普遍,您可能會遇到這樣一種情況:實體之間存在多個關系。在這種情況下,Code First將不能夠與相關導航屬性相匹配。您將需要提供一些額外的配置。

例如,如果你想跟蹤每個住所的兩個聯系人怎么辦?這就需要在Lodging類中有一個PromaryContact和一個SecondaryContact屬性。我們先將這兩個屬性添加到類中:

public Person PrimaryContact { get; set; }

public Person SecondaryContact { get; set; }

 

在關系的另一端我們也需要引入導航屬性。這需要讓你從Person類導航到Lodging實例,知道第一聯系人和第二聯系人連接到哪里去。添加如下兩個屬性到Person類:

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

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

 

Code First默認約定將對你剛才添加的這些新的關系進行錯誤的假設。因為有兩套導航屬性,Code First無法確定他們如何匹配,它會創建單獨為每個屬性創建關系。圖4-6顯示的Code First創建的基於您剛才添加的導航屬性的四個關系。

Code First默認規則可以識別雙向關系,但不能識別在兩個實體中多個雙向關系。原因如圖4-6所示,多個外鍵,使得Code First無法確定在Lodging中的兩個返回Person實體的屬性連接到Person類的哪個List<Lodging>屬性。

你可以添加配置(使用Data Annotations或Fluent API)到modelBuilder來表明這種關系。使用Data Annotations,你需要使用一個特性標記叫做InverseProperty。使用Fluent API,需要合並使用Has/With方法指定這些關系正確的端點。

你可將特性標記放在關系的任何一端(或兩端都放)。我們將其放在Lodging類中(代碼4-10)。InverseProperty特性標記需要相關類中相應導航屬性作為參數。

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

[InverseProperty("PrimaryContactFor")]

public Person PrimaryContact { get; set; }

[InverseProperty("SecondaryContactFor")]

public Person SecondaryContact { get; set; }

使用Fluent API,你需要使用Has/With語句來指定關系的兩端。見代碼4-11 ,第一個配置的一端為Lodging.PrimaryContact,另一端為Person.Primary ContactFor。第二個配置是針對SecondaryContact和SecondaryContactFor兩者關系建立的,方法類似。

Example 4-11. Configuring multiple relationships fluently

modelBuilder.Entity<Lodging>()

.HasOptional(l => l.PrimaryContact)

.WithMany(p => p.PrimaryContactFor);

modelBuilder.Entity< Lodging >()

.HasOptional(l => l.SecondaryContact)

.WithMany(p => p.SecondaryContactFor);

使用級連刪除

級聯刪除允許主記錄被刪除時相關聯的依賴性數據也被刪除。例如,如果你刪除Destinantion,相關的Lodgings會被自動刪除。EF框架支持對內存中和數據庫中的數據進行級聯刪除。在"用EF框架編程"第二版第19章,推薦為模型實體配置級聯刪除,所映射的數據庫對象也會具有級聯刪除的定義。

默認規則約定,Code First會對Required的關系設置級聯刪除。當一個級聯刪除定義后,Code First會在數據庫中為其創建級聯刪除。在本章前面我們已經將Lodging和Destination的關系設定為Required 。換句話說,沒有Destination,Lodging也不存在。因此,如果刪除一個Destination,任何相關聯的Lodging(在內存中且被上下文所跟蹤)也會被刪除。當提交SaveChanges,數據庫會刪除任何保存在Lodgings表中的相關行,使用的就是級聯刪除行為。

再看數據庫,你會看到Code First實施了級聯刪除並且在數據庫之間的關系上添加了約束。請注意圖4-7的刪除規則設定了級聯。

 

代碼4-12是一個新方法叫做DeleteDestinationInMemoryAndDbCascade,用於展示內存中和數據庫中的級聯刪除。

Example 4-12. A method to explore cascade deletes

private static void DeleteDestinationInMemoryAndDbCascade()

{

int destinationId;

using (var context = new BreakAwayContext())

{

var destination = new Destination

{

Name = "Sample Destination",

Lodgings = new List<Lodging>

{

new Lodging { Name = "Lodging One" },

new Lodging { Name = "Lodging Two" }

}

};

context.Destinations.Add(destination);

context.SaveChanges();

destinationId = destination.DestinationId;

}

using (var context = new BreakAwayContext())

{

var destination = context.Destinations

.Include("Lodgings")

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

var aLodging = destination.Lodgings.FirstOrDefault();

context.Destinations.Remove(destination);

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

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

context.SaveChanges();

}

}

 

代碼使用context插入了一個新Destination,有兩個Lodging.然后將這些Lodging儲存進數據庫然后記錄了新添加的Destination。在一個單獨的context里,代碼取出Destination和其相關的Lodging,然后使用Remove方法標記Destination實例為刪除,我們使用Console.WriteLine來檢測相關Lodging實例在內存中狀態,這使用了一個DbContext的Entry方法。Entry方法能夠讓我們訪問EF施加給給定對象的狀態信息。最后,調用SaveChanges方法持久化刪除信息到數據庫。

調用Destination的Remove方法,Lodging的狀態顯示在控制台窗口。盡管我們並沒有顯示地要求刪除任何Lodging,但仍顯示出了刪除命令。這是因為當我們顯示地刪除Destination時,EF框架使用客戶端的級聯刪除功能刪除了依賴的Lodging。

下一步,當SaveChanges方法調用時,EF框架發送三個DELERE命令到數據庫。如圖4-8所示,前兩刪除命令是對相關Lodging實例進行刪除,第三個才是刪除Destination,

現在我們來改變一下方法。我們將要要隨同Destination一起刪除以前存入的Loading數據。我們刪除與Lodging提到的所有相關代碼。由於內存中無Lodging,就不會有客戶端的級聯刪除,而數據庫卻清除了任何孤立的Lodgings數據,這是因為在數據庫中定義了級聯刪除。(見圖4-7)修改的方法見代碼4-13.

Example 4-13. Modified DeleteDestinationInMemoryAndDbCascade code

private static void DeleteDestinationInMemoryAndDbCascade()

{

int destinationId;

using (var context = new BreakAwayContext())

{

var destination = new Destination

{

Name = "Sample Destination",

Lodgings = new List<Lodging>

{

new Lodging { Name = "Lodging One" },

new Lodging { Name = "Lodging Two" }

}

};

context.Destinations.Add(destination);

context.SaveChanges();

destinationId = destination.DestinationId;

}

 

using (var context = new BreakAwayContext())

{

var destination = context.Destinations

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

context.Destinations.Remove(destination);

context.SaveChanges();

}

using (var context = new BreakAwayContext())

{

var lodgings = context.Lodgings

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

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

}

}

運行后,發送到數據庫的唯一命令是刪除destination。數據庫級聯刪除響應的相關Lodging。當在Lodgings端查詢時,由於數據庫刪除了lodgings,查詢不會返回結果,lodgings變量成為一空的列表。

使用Fluent API配置打開或關閉客戶端級聯刪除功能

你可能會在現有的數據庫上工作,不使用級聯刪除或者你可能有一個規則必須顯示刪除數據,不允許在數據庫中自動刪除。如果從LodgingDestination之間的關系是Optional,這不是一個問題,因為按照默認規則,Code First不能在可選的關系上使用級聯刪除。但你可能需要即有Required的關系,又不想使用級聯刪除功能。

例如需要在應用程序中試圖刪除一個Destination時向用戶返回一個錯誤,這是在沒有顯示地刪除或重新給這個Destination分配Lodging實例時出現的。在這種情況下,你就需要一個Required關系而不需要級聯刪除,你可以顯示地覆寫默認規則通過使用Fluent API來對級聯刪除進行配置。這個功能Data Annotations不支持。

記住,如果建立這樣的模型,應用程序代碼可以實現定制地刪除數據,或在必要時重新分配相關的數據。

Fluent API使用的方法是WillCascadeOnDelete,以一個布爾值作為參數。此配置適用於所有關系,因此首先需要使用指定一個配對的關系,然后調用WillCascadeOnDelete方法。

在LodgingConfiguration類中,關系定義為:

HasRequired(l=>l.Destination)

.WithMany(d=>d.Lodgings)

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

現在你可以設置此關系的WillCascadeOnDelete為false:

HasRequired(l=>l.Destination)

.WithMany(d=>d.Lodgings)

.WillCascadeOnDelete(false)

這就使得Code First生成的數據庫架構將不會包含級聯刪除。圖4-7所示的級聯刪除規則將不會出現。

在關系為Required的場景下,這種邏輯會創建一個沖突,例如,目前在LodgingDestination的Required關系中,需要一個Lodging實例有一個Destination或一個DestinationId。如果你有一個正在變化的跟蹤,並刪除了相關的Destination,這將導致Lodging.Destination為空。調用SaveChanges時,實體框架將嘗試同步Lodging.DestinationId,設置為NULL。但是,這是不行的,異常將拋出下面的詳細信息:

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

關系不能更新,因為一個或多個外鍵的屬性非空。對關系進行更新時,相關的外鍵的屬性設置為空值。如果外鍵不支持空值,必須定義一個新的關系,外鍵的屬性必須指派另一個非空值,或刪除非關聯的對象。

這表明,如果已經控制了級聯刪除設置,就要為避免或解決驗證沒有級聯刪除可能引起的沖突負責。

對不被數據庫所支持的場合關閉級聯刪除

許可數據庫(包括SQL Server)不支持指定級聯刪除指向到同一個表的多重關系(原文為:Some databases (including SQL Server) don't support multiple relationships that specify cascade delete pointing to the same table.不知如何翻譯---譯者注 )。由於Code First配置的Required關系包括級聯刪除,這就造成如果有兩個Required關系指向同一個實體就會出現錯誤。你可以使用WillCascadeOnDelete(false)來關閉一個或多個聯刪除設置。代碼4-14顯示了如果不進行正確配置來自於SQL Server的異常信息。

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

System.InvalidOperationException was unhandled

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

did not.

See InnerException for details.

InnerException: System.Data.SqlClient.SqlException

Message=Introducing FOREIGN KEY constraint 'Lodging_SecondaryContact' on table

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

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

constraints. Could not create constraint. See previous errors.

 

客戶端級聯刪除對性能的影響

本身就會實施級聯刪除。如果你將所有相關對象調入內存,讓客戶端級聯刪除這些對象,就會導致調用SaveChanges方法時,會發送很多針對這些相關對象的Delete命令到數據庫,從而造成暫時的命令擁塞。當然有的情況下,這些相關對象是在內存中的,也希望這些對象能夠被刪除,這種情況另當別論。但是,如果數據並沒載入內存,完全可以依靠數據庫做級聯刪除,而避免將這些對象載入內存。

探索多對多關系

EF框架支持多對多關系。讓我們來看看Code First是如何在生成數據庫時響應類間的多對多關系。

在使用database first策略時如果有多對多關系,EF框架可以創建多對多映射,條件是數據庫內聯表只包含相關實體的主鍵。這種映射規則也適用於Code First。

我們添加一個新的類:Acitivity到模型中,如代碼4-15,將於Trip類相關聯。一個Trip類可以有一些Activites日程,而一個Activity日程又可以計划好幾個trips(行程)。因此Trip和Activity就會有多對多關系。

Example 4-15. A new class, Activity

using System.ComponentModel.DataAnnotations;

using System.Collections.Generic;

namespace Model

{

public class Activity

{

public int ActivityId { get; set; }

[Required, MaxLength(50)]

public string Name { get; set; }

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

}

}

在Activity類中有一個List<Trip>,我們也添加了一個List<Activity>到Trip類到另一端形成多對多關系。

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

再次運行程序,因為模型變化Code First將重新創建數據庫。Code First根據默認規則識別出了多對多關系,建立了內聯表,並配置了合適的鍵。兩個內聯表的主鍵都作為外鍵指向了內聯表,如圖4-10所示。

注意到Code First的默認規則創建的表名合並使用了兩個類的類名。它也使用了我們在前面創建外鍵使用的模式來創建外鍵。在第5章,我們將關注於表和列的映射,到時你會學習到如何使用配置為內聯表指定表名和列名。

一旦多對對關系建立,其行為就與EF早期版本中多對多關系所表現出來的是一樣的。你可以通過類屬性查詢,添加和刪除相關對象。在后台,EF框架將使用它的內置特性來協助數據庫創建集成的內聯表的select,insert,update和delete命令。

例如,如下的查詢尋找一次單獨的trip和計划實施的相關Activities.

var tripWithActivities = context.Trips

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

查詢是對類進行的,沒有必要關心trip和activities是怎樣在數據庫連接的。EF框架會自行配置SQL語句執行內聯,並返所有適合於第一條trip的所有activities記錄。雖然不需要自行構建SQL語句,但一定要記住不管你的類的結構或數據庫構架如何,EF框架構建的SQL都是可以通用的。

輸出的結果是trip和其activities的圖。圖4-11顯示了Trip類在一個調試窗口的信息。你可以看到其包含兩個activites,都最從數據庫中提取出來匹配這次Trip的。

不必知道它的存在,EF框架會維護內聯表並通過來組織表之間的Join。同樣地,任何時候你進行插入,更新或刪除操作,EF框架將制定出正確的內聯SQL語句,不用在你的代碼中作任何關注。

使用單邊導航的關系

到目前為止我們已經觀察了導航屬性已經定義在兩個類中的關系。但是,這並不是EF框架能夠工作所必須的。

在你的域中,從Destination導航到其相關的Lodging選項是一種通常的情況,但是可能很少需要從Lodging導航回Destination.讓我們將Destination從Lodging類中移走(代碼4-16)。

Example 4-16. Navigation property removed from Lodging class

public class Lodging

{

public int LodgingId { get; set; }

public string Name { get; set; }

public string Owner { get; set; }

public bool IsResort { get; set; }

public decimal MilesFromNearestAirport { get; set; }

public int DestinationId { get; set; }

//public Destination Destination { get; set; }

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

public Person PrimaryContact { get; set; }

public Person SecondaryContact { get; set; }

}

EF框架可以處理這種情況。這里清晰地定義了從Lodging到Destination之間的關系,依據的是Destination類中的Lodgings屬性。這仍然會使用模型構建器到Lodging類中去尋找外鍵Lodging.DestinationId滿足默認規則。

現在我們前進一步,將Lodging類中的外鍵屬性刪除,如代碼4-17.

Example 4-17. Foreign key commented out

public class Lodging

{

public int LodgingId { get; set; }

public string Name { get; set; }

public string Owner { get; set; }

public bool IsResort { get; set; }

public decimal MilesFromNearestAirport { get; set; }

//public int DestinationId { get; set; }

//public Destination Destination { get; set; }

}

是否還記得如果不定義一個外鍵在你的類中Code First默認規則會自動引入一個?同樣的規則適用於在單邊定義的導航屬性。Destination仍然有一個屬性定義 了到Lodging的關系。圖4-13顯示了有一個Destination_DestinationId列加入到的Lodgings表中。這可能會使你回想起有關外鍵列的命名規則:[Navigation Property Name] + [Primary Key Name]。但是我們在Lodgin類里不再有一個導航屬性。如果在依賴實體中沒有導航屬性加以定義,Code First將會使用[Principal Type Name] + [Primary KeyName].在這種情況下,使用了同一個名字。

那么如果我們試圖在另一個類中只定義外鍵而沒有導航屬性呢,EF框架本身支持這種情況,但Code First不支持。Code First需要至少一個導航屬性來創建關系。如果你移除了兩邊的導航屬性,Code First將只將外鍵屬性作為任何類中的其他屬性而不會在數據庫中創建約束。

現在我將外鍵屬性調整為默認規則無法檢測到的情況。我們用LocationId替代DestinationId,如代碼4-16.記住我們沒有導航屬性,仍然被注釋着。

Example 4-18. Foreign key with unconventional name

public class Lodging

{

public int LodgingId { get; set; }

public string Name { get; set; }

public string Owner { get; set; }

public bool IsResort { get; set; }

public decimal MilesFromNearestAirport { get; set; }

public int LocationId { get; set; }

//public Destination Destination { get; set; }

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

public Person PrimaryContact { get; set; }

public Person SecondaryContact { get; set; }

}

感謝Destination.Lodgings,Code First知道兩個類中存在關系。但它無法找到一個符合約定的外鍵。我們之前已經鋪了路,現在還需要一些配置來幫助Code First識別外鍵。

在前面的例子里,我們將ForeignKey特性標記放在依賴類的導航屬性或者將其放在外鍵屬性上,告知哪個導航屬性屬於它。但我們在依賴類中不再有一個導航屬性。幸運的是,我們可以將Data Annotations的標記放在導航屬性上(Destination.Lodgings)。Code First知道Lodging是關系中的依賴類,因此它會為外鍵在此類中尋找有關字段:

[ForeignKey("LocationId")]

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

Fluent API也能為這種單側導航屬性創建關系。配置的Has部分必須指定一個導航屬性,而With部分如果沒有反向導航屬性就留空。一旦指定了Has和With語句,就可以調用HasForeignKey方法:

modelBuilder.Entity<Destination>()

.HasMany(d => d.Lodgings)

.WithRequired()

.HasForeignKey(l => l.LocationId);

在我們需要創建單邊關系時,很多情況下我們想要從Lodging導航回相應的Destination。我們恢復對Lodging類的調整。取消對Destination屬性的注釋並將外鍵屬性恢復,如代碼4-19.你也需要將ForegnKey標記從Destination.Lodging上移除,刪除上述剛剛添加的Fluent API配置。

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

public class Lodging

{

public int LodgingId { get; set; }

public string Name { get; set; }

public string Owner { get; set; }

public bool IsResort { get; set; }

public decimal MilesFromNearestAirport { get; set; }

public int DestinationId { get; set; }

public Destination Destination { get; set; }

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

public Person PrimaryContact { get; set; }

public Person SecondaryContact { get; set; }

}

使用一對一關系

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

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

有一種關系Code First必須進行配置后才能工作,這種關系就是一對一關系。當你在模型中定義一對一關系,你需要在每個類中都要使用引用導航。如果你有一個引用和一個集合,Code First就會將引用視為依賴類,推測應該有一個外鍵。如果有兩個集合,Code First視為多對多關系,將外鍵放在一個單獨的內聯表中。但是,Code First看到兩個引用時,它無法識別哪個類應該有一個外鍵。

我們添加一個新的PersonPhoto類,包含一個針對屬於Person類中的people的photo和caption屬性。由於photo將會指定給特定的person,我們使用PersonId作為鍵屬性。並有沒有一個默認的鍵屬性,需要如下所示的Data Annotations配置(代碼4-20):

Example 4-20. The PersonPhoto class

using System.ComponentModel.DataAnnotations;

namespace Model

{

public class PersonPhoto

{

[Key]

public int PersonId { get; set; }

public byte[] Photo { get; set; }

public string Caption { get; set; }

public Person PhotoOf { get; set; }

}

}

我們在Person中也添加一個Photo屬性,這樣可以在兩端都可以導航。

public PersonPhoto Photo { get; set; }

記住在這種情況下Code First無法確認哪個類是依賴類。當其嘗試構建模型時,就會拋出一個異常,告知你它需要更多信息:

Unable to determine the principal end of an association between the types 'Model.PersonPhoto' and 'Model.Person'. The principal end of this association must be explicitly configured using either the relationship fluent API or data annotations.
無法確認在類型'Model.PersonPhoto'和'Model.Person'之間聯系的主端。這種聯系的主端必須使用Fluent API或data annotations進行顯示配置;

個問題可以很容易地使用ForeignKey特性標記來解決,將標記放在依賴類上指出其包含外鍵。當配置一對一關系時,EF框架需要依賴類的主鍵也應是外鍵。在我們的案例中,PersonPhoto是依賴類,而其鍵,PersonPhoto.PersonId,也應是一個外鍵。我們將ForeignKey標記加在PersonPhoto.PersonId屬性上,如代碼4-21,記住在加入ForeignKey時要為關系指定導航屬性。

 

Example 4-21. Adding the ForeignKey annotation

public class PersonPhoto

{

[Key]

[ForeignKey("PhotoOf")]

public int PersonId { get; set; }

public byte[] Photo { get; set; }

public string Caption { get; set; }

public Person PhotoOf { get; set; }

}

運行程序會成功創建新數據庫標,盡管你會看到EF框架並沒有很好地處理單詞"Photo",還是將其復數化(第5章你會學習如何為表指定名稱),但是PersonId現在即是PK又是FK。如果你觀察PersonPhoto_PhotoOf外鍵約束細節,還可以看到這里顯示People.PersonId在關系中是主表/列,而PersonPhotoes.PersonId是外鍵表/列(圖4-14):

在本章前面,介紹過可以將ForeignKey標記放在導航屬性上,也可以指定外鍵屬性的名稱(在本例中,就是PersonId).由於兩個類都包含PersonId屬性,Code First仍不能確認哪個類包含外鍵,因此你不能用這樣的方式來為此種場景配置。

當然,我們也可以Fluent API來進行配置。我們假定這時的關系是一對零或一對一,也就是PersonPhoto必須有一個Person對應而一個Person不必一定有一個PersonPhoto對應。我們使用HasRequired和WithOptinal聯合使用來指定這種情況:

modelBuilder.Entity<PersonPhoto>()

.HasRequired(p => p.PhotoOf)

.WithOptional(p => p.Photo);

這足以讓Code First將PersonPhoto視作依賴類。我們想要將Person類作為主類而PersonPhoto輔助類,因為一個Person可以存在沒有PersonPhoto的情況,但是一個PersonPhoto必須有一個Person.

注意你沒有必要使用HasForeignKey來指定PersonPhot.PersonId作為外鍵。這是因為EF框架可以直接將依賴項的主鍵作為外鍵使用。由於沒有選擇,Code First會將這種唯一情況推斷出來。事實上,Fluent API也不會讓你使用HasForeignKey,在HasRequired和WithOptional方法后的智能感知里該方法根本不可用。

當兩端都是Required時配置一對一關系

現在我們來告訴Code First一個Person必須有一個PersonPhoto(即也是Required)。使用Data Annotations,可以將Rrequired標記放在任何類型的屬性上來實現(不一定非是原生類型):

[Required]

public PersonPhoto Photo { get; set; }

現在更新Main方法來調用InserPerson方法(見第3章),運行程序。在運行SaveChanges時會拋出異常,EF框架的驗證API報告對PersonPhoto的Required要求驗證失敗。

Ensuring that the sample code honors the required Photo

如何修正代碼?

如果你想讓Photo屬性為Required又要避免驗證錯誤,可以修改InsertPerson和UpdatePerson方法以便可將數據添加到Photo字段中。為了保持代碼的簡潔,我們只填充一個單一的字節到Photo的byte數組里而不是使用實際的圖片。

在InsertPerson方法里,修改代碼實例化一個新的Person對象添加Photo屬性,如代碼4-22:

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

var person = new Person

{

FirstName = "Rowan",

LastName = "Miller",

SocialSecurityNumber = 12345678,

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

};

在UpdatePerson方法中,我們添加了一些代碼來保證任何已添加的Person數據都會在更新時同時獲得一個Photo。修改UpdatePerson方法見代碼4-23:

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

private static void UpdatePerson()

{

using (var context = new BreakAwayContext())

{

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

person.FirstName = "Rowena";

if (person.Photo == null)

{

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

}

context.SaveChanges();

}

}

更新方法使用Include方法來獲取數據庫中Person的圖片。然后檢查Person對象是否有Photo數據,如果沒有就添加一個新的。現在Person類中的Photo的Required要求得到滿足,就可以在任何時候成功執行InsertPerson和UpdatePerson方法。

使用Fluent API配置一對一關系

毫無疑問,也可以使用Fluent API來配置同樣的關系。但首先需要讓Code First知道哪個類為主哪個類為輔。如果兩端均為Required,不能簡單地從多重關系上推測出來。

你可以跟隨在WidthRequired后面來調用HasRequired方法 。但是如果你開始於HasRequired,你會在WithReuired的位置有兩個附加選擇:WithRequiredPrincipal 和WithRequiredDependent。這些方法將你要配置的實體考慮了進去(就是你選擇的基於模型構建器或者EntityTypeConfiguration類建立的實體)。選擇WithRequiredPrincipal將會使實體配置為主類,意味着該類包含有關系的主鍵。選擇WithRequiredDependent會使實體配置為輔助類,意味着該類包含有關系的外鍵。

假設你想將PersonPhoto配置為依賴類,應該使用下列配置代碼:

modelBuilder.Entity<PersonPhoto>()

.HasRequired(p => p.PhotoOf)

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

配置兩端都是Optional的一對一的關系其方法是類似的,除了開始於HasOptional外還應該選擇是WithOptionalPrincipal 還是 WithOptionalDependent。

小結

 

在本章,你已經看到Code First在處理關系上很智能。Code First的默認規則能夠發現任何多樣性的關系,並適時提供外鍵配置。但也有一些場景可能你並不想完全遵循默認規則,Code First完全支持這種場景。你也學習了如何使用Data Annotations和Fluent API來定制模型。你應該已經很好地理解了如何在在Fluent API中使用基於Has/With的語句來處理關系。

在下一章,我們來看看Code First的另一套映射,就是類如何映射到數據庫,包括如何映射到各種繼承架構等。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM