在EntityFramework的世界裡,遇過最奇妙的部份其中一個就是多對多的轉置關係了,它非常好用,但也帶了一些副作用。
多對多其實在一、兩年前就已經遇過,最近又遇到類似的問題,剛好把這部份的資料整理起來跟大家分享
當資料有做正規化的話,有時候就會遇到多對多的處理情況
什麼是多對多呢?
最簡單的例子就是,每一個台灣人都可以擁有多重國藉
所以你會有使用者(Users, key: UserID), 國藉(Nations, Key: NationID)
以及使用者對應國藉(User2Nation, Key: UserID + NationID)這三個資料表
很習慣地我們會把這三個資料表加到EntityFramework designer頁面去
然而你會發現一個奇怪的景象
User2Nation不見了!!!
User2Nation不見了!!!
User2Nation不見了!!!
取而代之的只有Users與Nations這兩個資料表,但這不是Bug
在navigation property中,也可以很清楚發現這是沒有問題的
public partial class Users
{
public Users()
{
this.ExpenseDetail = new HashSet<ExpenseDetail>();
this.Nations = new HashSet<Nations>();
}
public int userID { get; set; }
public string userName { get; set; }
public virtual ICollection<ExpenseDetail> ExpenseDetail { get; set; }
public virtual ICollection<Nations> Nations { get; set; }
}
我們馬上就來測試這個方便的功能
using (StreamWriter sw = new StreamWriter(string.Format(@"{0}\log.txt", logPath)))
{
using (var db = new NorthwindEntities())
{
db.Database.Log = s => Log(sw, s);
//db.Database.Log = Console.Write;
var result = db.Users
.Where(m => m.Nations.Count > 1)
.Select(m => new
{
Nmae = m.userName,
lstNation = m.Nations
}).ToList();
foreach (var elment in result)
{
Console.WriteLine("User:" + elment.Nmae);
foreach (var nation in elment.lstNation)
Console.WriteLine("=====>" + nation.NationName);
}
}
}
若對於把sql log寫到folder 或是sql效能有疑問者可以參考前一篇 EntityFramework 效能檢視
效能上因為有加FK,所以沒有太大問題,在使用上又更上一層樓
但請注意天下沒有白吃的午餐,這麼好用的東西當然也會有要付出代價的地方
Create
新增的方式其實很簡單
如果你要針對一個新的User增加新的國藉
只要使用newUser.Nations.Add( new country object ) 即可
EntityFramework就會在User2Nation與Nation這兩個表格做Insert
如果我們用以下的方法Create一個新的User,會發生什麼事呢?
Nations tw = new Nations() { NationID = "TW", NationName = "Taiwan" };
Nations gr = new Nations() { NationID = "GR", NationName = "Greece" };
//insert user and nations
Users newUser = new Users() { userID = 99, userName = "new99" };
//TW exist
newUser.Nations.Add(tw);
//GR not exist
newUser.Nations.Add(gr);
db.SaveChanges();
Insert TW這個國藉的時候就會失敗,為什麼呢?
原因很簡單,因為TW這個國藉在Nations已經存在
在處理多對多的時候,最麻煩的就是我們得知道哪些資料是已經存在,哪些是不存在
因為TW已經存在於Nations,所以我們得改tw的來源
Nations tw = db.Nations.Where(m => m.NationID == "TW").FirstOrDefault();
Nations gr = new Nations() { NationID = "GR", NationName = "Greece" };
//insert user and nations
Users newUser = new Users() { userID = 99, userName = "new99" };
//TW exist
newUser.Nations.Add(tw);
//GR not exist
newUser.Nations.Add(gr);
db.SaveChanges();
這樣子EntityFramework才知道TW是從已經存在的資料抓出來的,它沒必要去Insert這筆資料
以這個例子來看,Users會Insert 一筆,User2Nation會Insert 2筆 (TW, GR),而Nations 也只會Insert一筆 (GR)
Delete
在講Update之前,我們先來看看Delete
Users user99 = db.Users.Where(m => m.userID == 99).FirstOrDefault();
user99.Nations.Clear();
db.SaveChanges();
User2Nation會Delete 2筆 (TW, GR), Users跟Nations不變
Update
之所以會先講Delete再講Update,你可能猜到為什麼了
由於是多對多的關係,User2Nation的欄位通常為兩個FK所組成的Key
在EntityFramework中Key是無法被更改的
Update = Delete + Insert
由於Entity Framework是有包Trasaction的,所以並不會有Delete成功,但Insert failed的情況,要嘛就是全部成功全部失敗
所以Update我們可以這樣做,就會長成如下方這樣的形式 (假設我們要更新10號User的國藉, 再假設TW, GR, JP這三個國家都存在)
//update
//delete and then insert back
Users user10 = db.Users.Where(m => m.userID == 10).FirstOrDefault();
Nations tw = db.Nations.Where(m => m.NationID == "TW").FirstOrDefault();
Nations gr = db.Nations.Where(m => m.NationID == "GR").FirstOrDefault();
Nations jp = db.Nations.Where(m => m.NationID == "JP").FirstOrDefault();
user10.Nations.Clear();
user10.Nations.Add(tw);
user10.Nations.Add(gr);
user10.Nations.Add(jp);
db.SaveChanges();
並把SQL log抓出來看看
Opened connection at 2017/4/17 下午 01:26:16 +08:00
SELECT TOP (1)
[Extent1].[userID] AS [userID],
[Extent1].[userName] AS [userName]
FROM [dbo].[Users] AS [Extent1]
WHERE 10 = [Extent1].[userID]
-- Executing at 2017/4/17 下午 01:26:16 +08:00
-- Completed in 13 ms with result: SqlDataReader
Closed connection at 2017/4/17 下午 01:26:16 +08:00
Opened connection at 2017/4/17 下午 01:26:16 +08:00
SELECT TOP (1)
[Extent1].[NationID] AS [NationID],
[Extent1].[NationName] AS [NationName]
FROM [dbo].[Nations] AS [Extent1]
WHERE 'TW' = [Extent1].[NationID]
-- Executing at 2017/4/17 下午 01:26:16 +08:00
-- Completed in 7 ms with result: SqlDataReader
Closed connection at 2017/4/17 下午 01:26:16 +08:00
Opened connection at 2017/4/17 下午 01:26:16 +08:00
SELECT TOP (1)
[Extent1].[NationID] AS [NationID],
[Extent1].[NationName] AS [NationName]
FROM [dbo].[Nations] AS [Extent1]
WHERE 'GR' = [Extent1].[NationID]
-- Executing at 2017/4/17 下午 01:26:16 +08:00
-- Completed in 33 ms with result: SqlDataReader
Closed connection at 2017/4/17 下午 01:26:16 +08:00
Opened connection at 2017/4/17 下午 01:26:16 +08:00
SELECT TOP (1)
[Extent1].[NationID] AS [NationID],
[Extent1].[NationName] AS [NationName]
FROM [dbo].[Nations] AS [Extent1]
WHERE 'JP' = [Extent1].[NationID]
-- Executing at 2017/4/17 下午 01:26:16 +08:00
-- Completed in 32 ms with result: SqlDataReader
Closed connection at 2017/4/17 下午 01:26:16 +08:00
Opened connection at 2017/4/17 下午 01:26:16 +08:00
SELECT
[Extent2].[NationID] AS [NationID],
[Extent2].[NationName] AS [NationName]
FROM [dbo].[User2Nation] AS [Extent1]
INNER JOIN [dbo].[Nations] AS [Extent2] ON [Extent1].[NationID] = [Extent2].[NationID]
WHERE [Extent1].[UserID] = @EntityKeyValue1
-- EntityKeyValue1: '10' (Type = Int32, IsNullable = false)
-- Executing at 2017/4/17 下午 01:26:16 +08:00
-- Completed in 23 ms with result: SqlDataReader
Closed connection at 2017/4/17 下午 01:26:16 +08:00
Opened connection at 2017/4/17 下午 01:26:16 +08:00
Started transaction at 2017/4/17 下午 01:26:16 +08:00
INSERT [dbo].[User2Nation]([UserID], [NationID])
VALUES (@0, @1)
-- @0: '10' (Type = Int32)
-- @1: 'GR' (Type = AnsiString, Size = 10)
-- Executing at 2017/4/17 下午 01:26:16 +08:00
-- Completed in 23 ms with result: 1
INSERT [dbo].[User2Nation]([UserID], [NationID])
VALUES (@0, @1)
-- @0: '10' (Type = Int32)
-- @1: 'JP' (Type = AnsiString, Size = 10)
-- Executing at 2017/4/17 下午 01:26:16 +08:00
-- Completed in 13 ms with result: 1
INSERT [dbo].[User2Nation]([UserID], [NationID])
VALUES (@0, @1)
-- @0: '10' (Type = Int32)
-- @1: 'TW' (Type = AnsiString, Size = 10)
-- Executing at 2017/4/17 下午 01:26:16 +08:00
-- Completed in 12 ms with result: 1
Committed transaction at 2017/4/17 下午 01:26:17 +08:00
Closed connection at 2017/4/17 下午 01:26:17 +08:00
在這邊我們可以發現,如果User2Nation筆數夠大的話,效率並不會太快
因為Entity Framework是一筆一筆塞的,並不是採用Bulk Insert的方式來做
但若是筆數不多的情況下,用這樣的方式處理並不會有太大的效能問題
參考資料
https://www.codeproject.com/tips/893609/crud-many-to-many-entity-framework