2017/12/18

使用 AutoMapper 後的 Entity 更新,須注意 EntityState 的異動

使用 EF 及 AutoMapper,最方便的地方在於對應 Model 及 ViewModel 時,不用徒手煉鋼一筆筆將屬性值分別對應,只要透過簡單的語法,即可快速將內容進行對映。而這次在更新實體並寫入資料庫的情境下,卻發生再怎麼執行,資料庫的資料總是沒有更新的異常狀態。以下是超直覺的寫法:
//利用 EF 方式取出預計要異動的 _user 實體
User _user = context.User.Find(user.UserId);

//AutoMapper 設定,並處理對映邏輯
var mapper = new MapperConfiguration(cfg => cfg.CreateMap()
                        .BeforeMap(
                            (src, dest) =>
                            {
                                /* …異動邏輯處理… */
                            }
                            )).CreateMapper();

//進行 Mapping 後,取得異動後的 _user 實體
_user = mapper.Map<user>(user);

這段程式碼很直覺,將來源資料
_user
及 ViewModel
user
透過 AutoMapper 進行對映,並更新
_user
實體內容,接著透過
SaveChanged()
方法更新資料表。不過這段程式碼執行後並不會更新任何異動,直覺反應應該是實體狀態 (EntityState) 出現了問題,因此接著在
SaveChanges()
加入以下程式碼:
var entry = context.Entry(_user);

若有下中斷點,會發現原先預估 EntityState 應為 Modified (物件中的一個純量變數已修改),結果卻得到 Detached (物件存在,但是沒追蹤此物件):


這下大致上知道出錯原因在哪了。原來
_user
經過 AutoMapper 異動後,EntityState 已被移除追蹤,難怪 EF 再怎麼儲存異動都沒有任何作用。為了解決這個問題,此時又出現另一個直覺:「只要把狀態更改至正確的狀態,問題就解決了嘛!」,因此在
SaveChange()
前加了以下程式碼:
var entry = context.Entry(_user);

if (entry.State == EntityState.Detached)
{
  //把 _user 的狀態由 "Detached" 改為 "Modified"
  _user.State = EntityState.Modified;
}

接著出現了這個錯誤訊息:
Attaching an entity of type 'Project.Entities.User' failed because another entity of the same type already has the same primary key value. This can happen when using the 'Attach' method or setting the state of an entity to 'Unchanged' or 'Modified' if any entities in the graph have conflicting key values. This may be because some entities are new and have not yet received database-generated key values. In this case use the 'Add' method or the 'Added' entity state to track the graph and then set the state of non-new entities to 'Unchanged' or 'Modified' as appropriate.
由於我們作業一開始已自
context
先將
_user
內容取出,之後因為作業因素,再將透過 Post 或其他方式取得的 ViewModel 加到
context
裡,此時
context
便會有兩筆相同 PKey 的資料,違反了資料表準則,因此就會出現這個錯誤。

為了排除這個問題,可透過以下程式碼直接將
_user
的 EntityState 進行變更:
//Mapper 後判斷與調整實體狀態
var entry = context.Entry(_user);

if (entry.State == EntityState.Detached)
{
  var set = context.Set<User>();
  User attachedEntity = set.Find(_user.UserId);

  if (attachedEntity != null)
  {
    var attachedEntry = context.Entry(attachedEntity);
    attachedEntry.CurrentValues.SetValues(_user);
  }
  else
  {
    entry.State = EntityState.Modified;
  }
}

context.SaveChanges();

首先跟先前錯誤的直覺程式碼一樣,資料 Mapper 後先判斷當前
_user
的 EntityState。若發現是 Detached (未追蹤) 狀態,先至
context
重新取得 User 內容 (也就是
attachedEntity
),若
attachedEntity
有內容,便將目前已 Mapper 後但未有追蹤狀態的
_user
內容指派到新建立的
attachedEntry
實體,最後透過
attachedEntry
的 StateEntity 進行異動更新,這時便大功告成了。

完整範例程式碼如下:
//利用 EF 方式取出預計要異動的 _user 實體
User _user = context.User.Find(user.UserId);

//AutoMapper 設定,並處理對映邏輯
var mapper = new MapperConfiguration(cfg => cfg.CreateMap<User, User>()
                        .BeforeMap(
                            (src, dest) =>
                            {
                                /* …異動邏輯處理… */
                            }
                            )).CreateMapper();

//進行 Mapping 後,取得異動後的 _user 實體
_user = mapper.Map<User>(user);

//Mapper 後判斷與調整實體狀態
var entry = context.Entry(_user);
if (entry.State == EntityState.Detached)
{
  var set = context.Set<User>();
  User attachedEntity = set.Find(_user.UserId);

  if (attachedEntity != null)
  {
    var attachedEntry = context.Entry(attachedEntity);
    attachedEntry.CurrentValues.SetValues(_user);
  }
  else
  {
    entry.State = EntityState.Modified;
  }
}

context.SaveChanges();

參考文件
  1.  EntityState Enumeration
  2.  An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key
  3.  Entity Framework 更新時出現「ObjectStateManager 中已經有具有相同索引鍵的物件。ObjectStateManager 無法追蹤多個具有相同索引鍵的物件。」錯誤

沒有留言 :

張貼留言

注意:只有此網誌的成員可以留言。