出處:http://www.cnblogs.com/scy251147/p/3688844.html
關於Entity Framework中的Attached報錯的完美解決方案終極版
前發表過一篇文章題為《關於Entity Framework中的Attached報錯的完美解決方案》,那篇文章確實能解決單個實體在進行更新、刪除時Attached的報錯,注意我這里說的單個實體,指的是要更新或刪除的實體不包含其它實體(比如導航屬性就包含其它實體),也就是簡單POCO對象;但如果不是呢?那么那篇文章里的方法在一定程度上不起作用了,仍會報錯,我開始也想不明白,明明通過IsAttached函數判斷要更新的實體並未Attached,但進行Attaching時但仍然報錯說有相同Key,開始還以為是MS的BUG,后經過多次反復調試發現,報錯是對的,因為他報的錯並不是我當前要更新的實體,而是該實體中關聯的實體,代碼與演示報錯如下:(僅是演示代碼)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
public
class
A
{
public
string
a{
get
;
set
;}
public
string
b{
get
;
set
;}
public
string
c{
get
;
set
;}
public
virtual
B b{
get
;
set
;}
}
public
class
B
{
public
string
x{
get
;
set
;}
public
string
y{
get
;
set
;}
public
string
z{
get
;
set
;}
}
var
a1= dbContext.Set<A>().Single();
a1.a=
"test1"
;
dbContext.SaveChanges();
dbContext.Detach(a1);
//從緩存中移除a1實體;
var
a2= dbContext.Set<A>().AsNoTracking().Single();
a2.a=
"test2"
;
dbContext.Set<A>().Attach(a2);
//報錯,說B相同的KEY已經有Attached
dbContext.Entry(entity).State = EntityState.Modified;
dbContext.SaveChanges();
|
針對這個報錯,我在想,為何查詢實體A的時候能同時關聯查詢實體B並都同時Attached到內存中,而當我執行Detach實體A時,卻沒能關聯Detach實體B,問題根源就在這里,知道這個原因了,現在就是要解決這個問題,如何解決呢?既然知道是Detach實體不全面造成的,那么我只需要獲取到當前DbContext上下文對象中現有的所有已Attached實體,在執行完相應的CRUD時,再全部依次Detach掉即可,解決方案代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
/// <summary>
/// 清空DB上下文中所有緩存的實體對象
/// </summary>
private
void
DetachedAllEntities()
{
var
objectContext = ((IObjectContextAdapter)
this
.baseContext).ObjectContext;
List<ObjectStateEntry> entries =
new
List<ObjectStateEntry>();
var
states =
new
[] { EntityState.Added, EntityState.Deleted, EntityState.Modified, EntityState.Unchanged };
foreach
(
var
state
in
states)
{
entries.AddRange(objectContext.ObjectStateManager.GetObjectStateEntries(state));
}
foreach
(
var
item
in
entries)
{
objectContext.Detach(item.Entity);
}
}
public
void
Commit()
//封裝的統一提交方法
{
this
.baseContext.SaveChanges();
this
.DetachedAllEntities();
//執行清除
}
|
在使用的時候配合之前那篇文章的IsAttached函數就能完美解決所有的Attached報錯問題了!
=======================================================================================
在Repository模式中,我的Update方法總是無法更新實體,這個非常郁悶,Update方法如下:
1: public virtual void Update(T entity)
2: {
3: try
4: {
5: if (entity == null) throw new ArgumentNullException("實體類為空");
6: Context.Entry(entity).State = EntityState.Modified;
7: //Context.SaveChanges();
8: }
9: catch (DbEntityValidationException dbex)
10: {
11: var msg = string.Empty;
12: foreach (var validationErrors in dbex.EntityValidationErrors)
13: foreach (var validateionError in validationErrors.ValidationErrors)
14: msg += string.Format("Property:{0} Error:{1}", validateionError.PropertyName, validateionError.ErrorMessage);
15:
16: var fail = new Exception(msg, dbex);
17: throw fail;
18: }
19: }
看上去是沒有任何問題的代碼,一旦有實體更新的時候,總會出現如下的錯誤提示:
看字面意思,好像是我的EntityState設置不正確導致的,雖然我嘗試過重新設置幾次EntityState,但是仍舊無法解決我的問題。
然后實在找不出原因,就利用關鍵字 “ EF Repository Update ”在Google上面搜集,果然找到一篇文章:Advanced Entity Framework 6 Scenarios for an MVC 5 Web Application (12 of 12),其中有一段話,提出了問題的所在:
This happened because of the following sequence of events:
The Edit method calls the ValidateOneAdministratorAssignmentPerInstructor method, which retrieves all departments that have Kim Abercrombie as their administrator. That causes the English department to be read. As a result of this read operation, the English department entity that was read from the database is now being tracked by the database context.
The Edit method tries to set the Modified flag on the English department entity created by the MVC model binder, which implicitly causes the context to try to attach that entity. But the context can't attach the entry created by the model binder because the context is already tracking an entity for the English department.
One solution to this problem is to keep the context from tracking in-memory department entities retrieved by the validation query. There's no disadvantage to doing this, because you won't be updating this entity or reading it again in a way that would benefit from it being cached in memory.
問題的原因如下:
在Context對象中,已經hold住了一個需要操作的對象,當我們把EntityState修改成modified的時候,Context會再次去加載那個操作對象,但是這樣加載是無法成功的,因為當前已經存在一個對象了,再加載會導致重復,然后拋出失敗的錯誤。
解決方法很簡單,就是在展示列表的時候,利用AsNoTracking將Hold住的對象釋放掉即可。我們修改代碼如下:
1: public virtual T Get(Expression<Func<T, bool>> where)
2: {
3: return Dbset.Where(where).AsNoTracking().FirstOrDefault<T>();
4: }
5:
6: public virtual IQueryable<T> GetMany(Expression<Func<T, bool>> where)
7: {
8: return Dbset.Where(where).AsNoTracking();
9: }
然后提交,OK,問題解決。
=============================Update 2014.09.19======================
看到評論中有朋友雖然按照上述方法,但是仍然無法解決這一問題。原因是在Context中還保留有當前實體的副本所致,這里只要我們將實體副本從內存中完全移除,就可以了。
//用於監測Context中的Entity是否存在,如果存在,將其Detach,防止出現問題。 private Boolean RemoveHoldingEntityInContext(T entity) { var objContext = ((IObjectContextAdapter)_context).ObjectContext; var objSet = objContext.CreateObjectSet<T>(); var entityKey = objContext.CreateEntityKey(objSet.EntitySet.Name, entity); Object foundEntity; var exists = objContext.TryGetObjectByKey(entityKey, out foundEntity); if (exists) { objContext.Detach(foundEntity); } return (exists); }
然后在Repository中,在進行更新和刪除之前,運行一下即可:
public T Remove(T entity) { try { RemoveHoldingEntityInContext(entity); _context.DbSet<T>().Attach(entity); return _context.DbSet<T>().Remove(entity); } catch (DbEntityValidationException dbex) { var msg = string.Empty; foreach (var validationErrors in dbex.EntityValidationErrors) foreach (var validateionError in validationErrors.ValidationErrors) msg += string.Format("屬性:{0} 錯誤:{1}", validateionError.PropertyName, validateionError.ErrorMessage); var fail = new Exception(msg, dbex); throw fail; } } public T Update(T entity) { try { RemoveHoldingEntityInContext(entity); var updated = _context.DbSet<T>().Attach(entity); _context.DbContext.Entry(entity).State = EntityState.Modified; return updated; } catch (DbEntityValidationException dbex) { var msg = string.Empty; foreach (var validationErrors in dbex.EntityValidationErrors) foreach (var validateionError in validationErrors.ValidationErrors) msg += string.Format("屬性:{0} 錯誤:{1}", validateionError.PropertyName, validateionError.ErrorMessage); var fail = new Exception(msg, dbex); throw fail; } }