AppBox升級進行時 - Attach陷阱(Entity Framework)

三生石上(FineUI控制元件)發表於2013-09-17

AppBox 是基於 FineUI 的通用許可權管理框架,包括使用者管理、職稱管理、部門管理、角色管理、角色許可權管理等模組。

Attach方法

前面我們已經多次使用Attach方法,上一次使用Attach方法修改使用者所屬部門的程式碼如下所示:

if (String.IsNullOrEmpty(hfSelectedDept.Text))
{
    item.Dept = null;
}
else
{
    int newDeptID = Convert.ToInt32(hfSelectedDept.Text);
    if (item.Dept.DeptID != newDeptID)
    {
        Dept newDept = new Dept { DeptID = newDeptID };
        DB.Depts.Attach(newDept);
        item.Dept = newDept;
    }
}

其中 newDeptID 是透過彈出視窗或者下拉選單選擇的部門ID,也就是說這個 newDeptID 其實是存在於資料庫中的,只不過還沒有被載入到記憶體中。

這種情況非常適合使用 Attach 方法,從而避免了一次資料庫查詢來生成Dept物件:

Dept dept = DB.Depts.Where(d => d.ID == newDeptID).FirstOrDefault();

取而代之,我麼使用Attach方法,就好像這個部門已經被載入到記憶體中一樣:

Dept newDept = new Dept { DeptID = newDeptID };
DB.Depts.Attach(newDept);

官方對Attach的解釋:http://msdn.microsoft.com/en-us/library/system.data.entity.dbset.attach(v=vs.103).aspx

Attach is used to repopulate a context with an entity that is known to already exist in the database. SaveChanges will therefore not attempt to insert an attached entity into the database because it is assumed to already be there. Entities that are already in the context in some other state will have their state set to unchanged. Attach is a no-op if the entity is already in the context in the unchanged state.  

簡單的翻譯:Attach用來將某個已知存在於資料庫中的實體重新載入到上下文中。SaveChanges不會嘗試將Attached的實體插入到資料庫中,因為這個實體假設已經存在於資料庫中。  

  

Attach陷阱

為了更好的說明使用Attach過程中可能會遇到的問題,我麼從如下簡單的例子入手:

User admin = DB.Users.Where(u => u.Name == "admin").FirstOrDefault();

Dept dept = new Dept { ID = 1 };
DB.Depts.Attach(dept);

admin.Dept = dept;
DB.SaveChanges();

這個示例完成了如下操作:

1. 從資料庫中載入使用者名稱為 admin 的使用者;

2. 將ID為1的部門附加到EF上下文中;

3. 設定admin使用者所屬的部門為上述部門;

4. 儲存改變。

 

如果第一次執行這段程式碼,會有兩個對資料庫的操作,如下圖所示:

  

 

如果第二次執行這段程式碼,由於已經將使用者資料載入到EF上下文,所以對使用者部門的更新不會做任何改變,資料庫操作只有一次查詢操作:

  

一切看似沒有任何問題,直到我們遇到如下程式碼:

DB.Depts.Find(1);

User admin = DB.Users.Where(u => u.Name == "admin").FirstOrDefault();

Dept dept = new Dept { ID = 1 };
DB.Depts.Attach(dept);

admin.Dept = dept;
DB.SaveChanges();

在這段程式碼中,ID為1的部門已經被載入到EF上下文中,此時再次Attach同一個物件,就會出錯:

  

 

跳出Attach陷阱

解決辦法也很簡單,我們需要現在EF的Local快取中查詢物件,如果找不到再Attach新物件。關於Local物件的詳細資訊:http://msdn.microsoft.com/en-us/data/jj592872

 

為了方便程式碼呼叫,我們在頁面基類PageBase中增加了一個Attach方法:

// 附加實體到資料庫上下文中(首先在Local中查詢實體是否存在,不存在才Attach,否則會報錯)
protected T Attach<T>(int keyID) where T : class, IKeyID, new()
{
	T t = DB.Set<T>().Local.Where(x => x.ID == keyID).FirstOrDefault();
	if (t == null)
	{
		t = new T { ID = keyID };
		DB.Set<T>().Attach(t);
	}
	return t;
}

  

因此完成上述示例正確的程式碼為:

DB.Depts.Find(1);

User admin = DB.Users.Where(u => u.Name == "admin").FirstOrDefault();

Dept dept = Attach<Dept>(1);
admin.Dept = dept;
DB.SaveChanges();

  

你可能也注意到了,我們為實體類增加了一個 IKeyID 的介面,這是我們手工增加的:

public interface IKeyID
{
	int ID { get; set; }

}

 

Dept實體類實現了 IKeyID 介面,定義如下所示:

public class Dept : IKeyID
{
	[Key]
	public int ID { get; set; }

	[Required, StringLength(50)]
	public string Name { get; set; }

	[Required]
	public int SortIndex { get; set; }

	[StringLength(500)]
	public string Remark { get; set; }

	
	
	public virtual Dept Parent { get; set; }
	public virtual ICollection<Dept> Children { get; set; }


	public virtual ICollection<User> Users { get; set; }

}

  

深入理解Attach

1. DBContext的作用域 

大家要理解一點,之所以出現上述異常,是因為我們將 DBContext 的例項儲存在 HttpContext 中,在本系列的第一篇文章就有描述(One DbContext per Request)。

因此,我們很難在Attach時得知此物件是否已經被載入到EF的上下文中。相反,如果使用如下程式碼,則可能就不會遇到那個異常了(我們明確知道DBContext的作用域,並知道其中載入了哪些實體物件):

using(var db = new AppBoxContext())
{
	db.Depts.Find(1);
}

using(var db = new AppBoxContext())
{
	User admin = db.Users.Where(u => u.Name == "admin").FirstOrDefault();

	Dept dept = new Dept { ID = 1 };
	db.Depts.Attach(dept);
	admin.Dept = dept;
	
	db.SaveChanges();
}

  

2. 不要對Attach抱有過多幻想

有些時候,我們可能對Attach的期望值過高了,比如下面程式碼:

User admin = DB.Users.Where(u => u.Name == "admin").FirstOrDefault();

Dept dept = new Dept { Name = "研發部" };
DB.Depts.Attach(dept);

admin.Dept = dept;
DB.SaveChanges();

我們本來希望是將admin使用者的部門設為“研發部”,可惜這段程式碼會報錯:

看錯誤提示,我們知道是執行的SQL語句違反了外來鍵約束。

檢視執行的SQL語句,我們會發現EF試圖將ID為0的部門更新到使用者表。實際上,Depts表不存在ID為0的部門!

 

其實,我們建立部門的程式碼:

Dept dept = new Dept { Name = "研發部" };

建立了一個ID為0的部門。EF並不會對此有效性進行檢查,更不會查詢資料庫獲取此部門的ID。EF的會假設這個實體已經存在於資料庫中了,而我們開發人員需要保證這個假設成立!  

 

 

下載或捐贈AppBox

1. AppBox v2.0 是免費軟體,免費提供下載:http://fineui.com/bbs/forum.php?mod=viewthread&tid=3788 

2. AppBox v3.0 是捐贈軟體,你可以透過捐贈作者來獲取AppBox v3.0的全部原始碼(http://fineui.com/donate/)。

 

 

 

相關文章