我們在使用Entity Framework進行CRUD時,為了提升查詢效率,一般均會啟動NoTracking,即不追蹤變化,設置代碼如下:
//這是DB First模式下設置方法: aTestEntities db = new aTestEntities(); db.Companies.MergeOption = MergeOption.NoTracking; //這是CODE First及Model First模式下設置方法: aTestEntities db = new aTestEntities(); db.Companies.AsNoTracking();
雖然啟動NoTracking,查詢效率提高了,但我們在進行CUD時,有時又會出現如下之類的報錯:
無法附加此對象,因為它已經在對象上下文中。對象只有在處於未更改狀態時才能重新附加。
因為查詢時啟用了NoTracking,即表明查詢的實體對象是處於Detached,我們再進行CRD時,必須先進行Attach操作,然后才能執行相應的增加、更新、刪除操作,但由於在有些情況下我們並不能保證需要進行CRD的實體為Detached,所以易造成重復Attach,從而導致報上面的錯誤或其它錯誤。
若要避免重復Attach,我們則必需要有一個能夠判斷實體的狀態是否為Attach,如果已Attached,我們就不需要再進行Attach操作,EF中並沒有這類的方法,所以我這里總結了如下幾個方案(IsAttached方法),可以避免此類問題的發生:
方案一:采用DB First時,由於實體類均繼承自EntityObject,所以我們可以通過EntityObject.EntityKey屬性來進行判斷
/// <summary>
/// 判斷entity是否已經Attached
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
public bool IsAttached<TEntity>(TEntity entity) where TEntity : EntityObject
{
ObjectStateEntry entry = null;
if (dbContext.ObjectStateManager.TryGetObjectStateEntry(entity.EntityKey, out entry))
{
if (entry.State != EntityState.Detached)
{
return true;
}
}
return false;
}
方案二:采用Model First或Code First時,由於實體類為我們自己設計的,默認並沒有繼承自EntityObject,所以就不能使用上面的方法,但我們可以以方案一中的思想,來設計實體類,我們可以定義一個接口IEntityWithId,然后讓所有的實體類均實現該接口,最后再改寫方案一的方法即可完成
public interface IEntityWithId
{
Guid Id { get; set; }
}
public class EntityClass : IEntityWithId
{
Guid Id { get; set; }
//...其它屬性
}
/// <summary>
/// 判斷entity是否已經Attached
/// </summary>
/// <typeparam name="TEntity"></typeparam>
/// <param name="entity"></param>
/// <returns></returns>
public bool IsAttached<TEntity>(TEntity entity) where TEntity : IEntityWithId
{
TEntity localEntity = dbContext.Set<TEntity>().Local.Where(t => t.Id == entity.Id).FirstOrDefault();
if (localEntity != null)
{
if (dbContext.Entry(localEntity).State != EntityState.Detached)
{
return true;
}
}
return false;
}
方案三:采用Model First或Code First時,若沒有定義統一的接口,那么我們就不能使用方案二中的IsAttached方法,這時該怎么辦呢?通過VS Debug時瀏覽實體對象發現,實體的類型並不是我們所定義的類型,而是變成了EntityWrapperWithoutRelationships<TEntity>,該類中有一個公共字段屬性:_entityWrapper,然后繼續查看該字段的類型,又發現了EntityWrapper類,該類中就有了EntityKey屬性,該屬性與方案一中的EntityObject.EntityKey屬性類型相同,如果我們能夠獲取到該EntityKey屬性,那么就可以使用方案一中的方法進行判斷了,但高興之余又發現,EntityWrapperWithoutRelationships及EntityWrapper類的訪問修飾符為internal,意味着我們並不能在自己的項目中直接使用,唯一的辦法就是采用反射來動態獲取該屬性,所以整個的實現方法如下:
/// <summary>
/// 判斷entity是否已經Attached
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
private bool IsAttached(TEntity entity)
{
var objectContext = ((IObjectContextAdapter)this.baseContext).ObjectContext;
ObjectStateEntry entry = null;
if (objectContext.ObjectStateManager.TryGetObjectStateEntry(GetEntityKey(entity), out entry))
{
if (entry.State != EntityState.Detached)
{
return true;
}
}
return false;
}
/// <summary>
/// 通過反射獲取實體對象的EntityKey
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
private EntityKey GetEntityKey(TEntity entity)
{
var entityWrapper = entity.GetType().GetField("_entityWrapper").GetValue(entity);//獲取字段_entityWrapper的值
var entityWrapperType = entityWrapper.GetType();//獲取字段的類型
var entityKey = entityWrapperType.GetProperty("EntityKey").GetValue(entityWrapper, null);//獲取EntityKey屬性的值
return (EntityKey)entityKey;
}
實現了IsAttached方法后,那么我們就再也不用擔心出現重復Attach的情況,使用方法很簡單,只需要在需要進行更新、刪除操作時前調用IsAttached方法判斷一下實體是否為Attached,若不是才Attach,否則忽略,代碼示例如下:
public virtual void Update(TEntity entity, bool autoCommit = false)
{
this.ValidateEntity(entity, false);
if (!this.IsAttached(entity))
{
this.objectSet.Attach(entity);
this.baseContext.Entry(entity).State = EntityState.Modified;
}
if (autoCommit)
{
this.Commit();
}
}
public virtual void Remove(TEntity entity, bool autoCommit = false)
{
if (!this.IsAttached(entity))
{
this.objectSet.Attach(entity);
}
this.objectSet.Remove(entity);
if (autoCommit)
{
this.Commit();
}
}
