2-3 無載荷(with NO Payload)的多對多關系建模
問題
在數據庫中,存在通過一張鏈接表來關聯兩張表的情況。鏈接表僅包含連接兩張表形成多對多關系的外鍵,你需要把這兩張多對多關系的表導入到實體框架模型中。
解決方案
我們設想,你數據庫中的表與圖2-10一樣。

圖2-10 藝術家和專輯多對多關系
按下面的步驟將這些表和關系導入到模型中:
1、右鍵你的項目,選擇Add(增加) ➤New Item(新建項),然后選擇Visual C#條目下的Data模板下的ADO.NET Entity Data Model(ADO.NET實體數據模型)。
2、選擇Generate from database 從一個已存在的數據庫創建模型,點擊Next(下一步)。
3、可以選擇一個已存在的數據庫連接,也可以選擇新建一個數據庫連接。
4、在選擇數據庫窗口,選擇表Album,LinkTable,以及Artist。然后勾選上確定所生成對象名稱的單復數形式、在模型中包含外鍵列復選框。點擊Finish(完成)。(譯注:步驟有省略,因為前面的小節已經有詳細步驟)
向導將創建如圖2-11所示的模型。

圖2-1 多對多關系模型
Album和Artist之間的多對多關系被表示成兩端帶字符*的直線。因為一份專輯會包含多位藝術家,而一位藝術家可能負責多份專輯。Album和Artist之間的導航屬性類型為EntityCollection.
原理
在圖2-11中,一個artit能關聯多個albums,反之,一個album也可能關聯多個artists。請注意,圖2-10中的鏈接表沒有出現在模型中。因為他沒有標量屬性(也就是說,它沒有載荷),實體框架認為它存在的唯一目的是關聯Album和Artist.如果這張鏈接表有標量屬性,實體框架將創建一個不同的模型,如下節所示。
代碼清單2-3演示,如何在我們的模型中插入albums和artists,以及如何從模型中查詢出artists和他們的albums,albums和他們的artists。
代碼清單2-3. 通過模型中的多對多關系插入和查詢我們的Artists和Albums
1 using (var context = new EF6RecipesContext()) { 2 3 // 添加一個擁有兩張專輯的藝術家 4 var artist = new Artist { FirstName = "Alan", LastName = "Jackson" }; 5 var album1 = new Album { AlbumName = "Drive" }; 6 var album2 = new Album { AlbumName = "Live at Texas Stadium" }; 7 artist.Albums.Add(album1); 8 artist.Albums.Add(album2); 9 context.Artists.Add(artist); 10 11 //添加兩個藝術家的專輯 12 var artist1 = new Artist { FirstName = "Tobby", LastName = "Keith" }; 13 var artist2 = new Artist { FirstName = "Merle", LastName = "Haggard" }; 14 var album = new Album { AlbumName = "Honkytonk University" }; 15 artist1.Albums.Add(album); 16 artist2.Albums.Add(album); 17 context.Albums.Add(album);
context.Artists.Add(artist1); //譯注:需要加上下面兩句(原書示例中,沒有這兩句),否則artist1和artist2不會保存
context.Artists.Add(artist2); 18 context.SaveChanges(); 19 } 20 21 using (var context = new EF6RecipesContext()) { 22 Console.WriteLine("Artists and their albums..."); 23 var artists = context.Artists; 24 foreach (var artist in artists) { 25 Console.WriteLine("{0} {1}", artist.FirstName, artist.LastName); 26 foreach (var album in artist.Albums) { 27 Console.WriteLine("\t{0}", album.AlbumName); 28 } 29 } 30 Console.WriteLine("\nAlbums and their artists..."); 31 var albums = context.Albums; 32 foreach (var album in albums) { 33 Console.WriteLine("{0}", album.AlbumName); 34 foreach (var artist in album.Artists) { 35 Console.WriteLine("\t{0} {1}", artist.FirstName, artist.LastName); 36 } 37 } 38 }
代碼清單2-3輸出:
Artists and their albums...
Alan Jackson
Drive
Live at Texas Stadium
Tobby Keith
Honkytonk University
Merle Haggard
Honkytonk University
Albums and their artists...
Drive
Alan Jackson
Live at Texas Stadium
Alan Jackson
Honkytonk University
Tobby Keith
Merle Haggard
創建數據庫上下文后,我們創建並初始化一個Artist的實例和兩個Album的實例。然后將albums增加到artist的導航屬性,並將artist實例添加到數據庫上下文中。
接下來,我們創建並初始化兩個實體類型Artist的實例和一個實體類型Album的實例。因為兩個藝術家合創一張專輯,所以,我們將album分別添加到兩個atists的導航屬性(它的類型為EntityCllection)中。將album添加到上下文對象,同時也會將兩個artists也添加到上下文。
現在對象圖已經是上下文中的一部份,只需要調用SaveChanges()方法保存到數據庫即可。
在新的上下文對象中查詢,獲取藝術家並顯示出他們的專輯。然后獲取專輯並打印出創作他們的藝術家。
注意,我們未提到圖2-10中的鏈接表,其實,它沒有作為一個實體出現在我們的模型中。鏈接表代表的多對多關聯,我們通過訪問Artists和Albums導航屬性即可完成。
2-4 有載荷的多對多關系建模
問題
數據庫中有多對多關系的表,它們通過擁有載荷數據(除外鍵外的任何列)的表進行關聯,你想創建一個代表這種多對多關系的模型,它將創建成兩個表示一對多的關聯。
解決方案
實體框架不支持帶有屬性的關聯,所以創建上一節那樣的模型,將不能實現我們的需求。正如上一節中所說的,如果一個鏈接表只擁有表示關系的外鍵,實體框架會把鏈接表呈現為一個關聯而不是一個實體類型。如果鏈接表包含額外的屬性,實體框架將呈現兩個單獨實體類型來表示鏈接表。結果就是,模型中包含了兩個一對多關聯的實體類型來表示底層數據庫中的鏈接表。
假設我們擁有如圖2-12所示的表及其關系

圖2-12 有載荷的多對多關系
一個訂單可以擁有多個訂單項,一個訂單項可以屬於多個訂單,在連接Order、Item實例的關系上有一個Count屬性,這個屬性被稱為一個有效載荷。
按下面的步驟將這些表和關系導入到模型中:
1、右鍵你的項目,選擇Add(增加) ➤New Item(新建項),然后選擇Visual C#條目下的Data模板下的ADO.NET Entity Data Model(ADO.NET實體數據模型)。
2、選擇Generate from database 從一個已存在的數據庫創建模型,點擊Next(下一步)。
3、可以選擇一個已存在的數據庫連接,也可以選擇新建一個數據庫連接。
4、在選擇數據庫窗口,選擇表Order,OrderItem,以及Item。后勾選上確定所生成對象名稱的單復數形式、在模型中包含外鍵列復選框。點擊Finish(完成)。(譯注:步驟有省略,因為前面的小節已經有詳細步驟)
向導將創建如圖2-13所示的模型。

圖2-13 從一個含有載荷的多對多關系建模成的兩個一對多關聯
原理
正如在上一小節中所說的那樣,有載荷的多對多關系,在模型中的導航簡單明了。因為實體框架不支持在關聯上附加載荷,鏈接表在模型中呈現為一個與其關聯實體有兩個一對多關聯的實體。因此,OrderItem不是被呈現為一個關聯,而是一個實體類型,該實體類型與實體類型Order有一對多的關聯,與實體類型Iterm有一對多的關聯。在前一小節,提到的無載荷的鏈接表在模型中沒有被轉化為實體類型,而是被轉化成了多對多關聯的一部份。
因為這個附加的載荷,訂單需要通過額外的一級(代表鏈接表的實體類型)來獲取與其相關聯的項。如代碼清單2-4所示。
代碼清單2-4 從模型插入和獲取 (下面的代碼有些問題,你能找出來了嗎?)
1 using (var context = new EF6RecipesContext()) { 2 var order = new Order { 3 OrderId = 1, 4 OrderDate = new DateTime(2010, 1, 18) 5 }; 6 var item = new Item { 7 SKU = 1729, 8 Description = "Backpack", 9 Price = 29.97M 10 }; 11 var oi = new OrderItem { Order = order, Item = item, Count = 1 }; 12 item = new Item { 13 SKU = 2929, 14 Description = "Water Filter", 15 Price = 13.97M 16 }; 17 oi = new OrderItem { Order = order, Item = item, Count = 3 }; 18 item = new Item { 19 SKU = 1847, 20 Description = "Camp Stove", 21 Price = 43.99M 22 }; 23 oi = new OrderItem { Order = order, Item = item, Count = 1 }; 24 context.Orders.Add(order); 25 context.SaveChanges(); 26 } 27 using (var context = new EF6RecipesContext()) { 28 foreach (var order in context.Orders) { 29 Console.WriteLine("Order # {0}, ordered on {1}", 30 order.OrderId.ToString(), 31 order.OrderDate.ToShortDateString()); 32 33 Console.WriteLine("SKU\tDescription\tQty\tPrice"); 34 Console.WriteLine("---\t-----------\t---\t-----"); 35 foreach (var oi in order.OrderItems) { 36 Console.WriteLine("{0}\t{1}\t{2}\t{3}", oi.Item.SKU, 37 oi.Item.Description, oi.Count.ToString(), 38 oi.Item.Price.ToString("C")); 39 } 40 } 41 }
經網友反映,原書中的示例有問題(具體可見后面的評論),從這里可以看出實踐的重要,有句話是這么說:“紙上得來終覺淺,絕知此事要躬行”。調整后的代碼如下:
using (var context = new Recipe4Context()) { var order = new Order { OrderId = 1, OrderDate = new DateTime(2010, 1, 18) }; var item = new Item { SKU = 1729, Description = "Backpack", Price = 29.97M }; var oi1 = new OrderItem { Order = order, Item = item, Count = 1 }; item = new Item { SKU = 2929, Description = "Water Filter", Price = 13.97M }; var oi2 = new OrderItem { Order = order, Item = item, Count = 3 }; item = new Item { SKU = 1847, Description = "Camp Stove", Price = 43.99M }; var oi3 = new OrderItem { Order = order, Item = item, Count = 1 }; context.OrderItems.Add(oi1); context.OrderItems.Add(oi2); context.OrderItems.Add(oi3); context.SaveChanges(); } using (var context = new Recipe4Context()) { foreach (var order in context.Orders) { Console.WriteLine("Order # {0}, ordered on {1}", order.OrderId.ToString(), order.OrderDate.ToShortDateString()); Console.WriteLine("SKU\tDescription\tQty\tPrice"); Console.WriteLine("---\t-----------\t---\t-----"); foreach (var oi in order.OrderItems) { Console.WriteLine("{0}\t{1}\t{2}\t{3}", oi.Item.SKU, oi.Item.Description, oi.Count.ToString(), oi.Item.Price.ToString("C")); } } } }
代碼清單2-4的輸入如下:
Order # 1, ordered on 1/18/2010 SKU Description Qty Price ---- ----------- --- ------ 1729 Backpack 1 $29.97 1847 Camp Stove 1 $43.99 2929 Water Filter 3 $13.97
創建數據庫上下文實例之后,我們創建了一個訂單(Order)實例、訂單項(Iterm)實例、(OrderIterm)訂單關聯項實例,我們使用OrderItem實體的實例連接order和iterms。最后,我們調用Add()方法,將orderItem添加到上下文中。
隨着orderItem被添加到上下文中,對象圖創建完成,我們調用SaveChanges()方法將其更新到數據庫。
為了從數據庫中獲取實體,我創建了一個新的數據庫上下文對象實例,然后迭代contntx.Orders集合,對於每個訂單(order)(當然,我們在示例中只有一個),通過迭代OrderItems導航屬性,打印出訂單的詳細信息。 OrderItem實體的實例可以讓我們直接訪問Count標量屬性(載荷),訂單上的項可以通過導航屬性Items訪問。通過OrderItems實體獲取訂單項items增加了一級訪問層次,這是有載荷鏈接表(示例中的OrderIterms)在多對多關系的代價。
最佳實踐
不幸的是,項目往往是以無載荷的多對多關系開始,卻以多載荷的多對多關系結束。重構一個模型,特別是開發周期的晚期,解決這種問題特別郁悶,不光是增加一個實體,查詢和關聯中導航模式也要改變。因此有一些開發人建議,每一個多對多關系一開始就包含一些載荷,作為一個合成鍵。以此減少對項目的影響。
所以,這里有這樣一個最佳實踐:如果你有一個無載荷的多對多關系時,你可以考慮通過增加一標識列將其改變為有載荷的多對多關系。當你導入表到你的模型時,你將得到兩個包含一對多關系的實體,這意味着,你的代碼為將來有可能出現的多載荷做好了准備。增加一整型標識列的代價通常很小,但給模型帶來了更大的靈活性。
這篇就這里吧。感謝你的閱讀!
實體框架交流QQ群: 458326058,歡迎有興趣的朋友加入一起交流
謝謝大家的持續關注,我的博客地址:http://www.cnblogs.com/VolcanoCloud/
