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/)。
文章列表