簡體   English   中英

EF Core - 在一個請求中添加/更新實體和添加/更新/刪除子實體

[英]EF Core - adding/updating entity and adding/updating/removing child entities in one request

我正在為似乎是幾個基本操作而苦苦掙扎。

假設我有一個名為 Master 的類:

public class Master
{
    public Master()
    {
        Children = new List<Child>();
    }

    public int Id { get; set; }
    public string SomeProperty { get; set; }

    [ForeignKey("SuperMasterId")]
    public SuperMaster SuperMaster { get; set; }
    public int SuperMasterId { get; set; }

    public ICollection<Child> Children { get; set; }
}

public class Child 
{
    public int Id { get; set; }
    public string SomeDescription { get; set; }
    public decimal Count{ get; set; }

    [ForeignKey("RelatedEntityId")]
    public RelatedEntity RelatedEntity { get; set; }
    public int RelatedEntityId { get; set; }

    [ForeignKey("MasterId")]
    public Master Master { get; set; }
    public int MasterId { get; set; }
}

我們有一個像這樣的控制器動作:

public async Task<OutputDto> Update(UpdateDto updateInput)
{
    // First get a real entity by Id from the repository
    // This repository method returns: 
    // Context.Masters
    //    .Include(x => x.SuperMaster)
    //    .Include(x => x.Children)
    //    .ThenInclude(x => x.RelatedEntity)
    //    .FirstOrDefault(x => x.Id == id)
    Master entity = await _masterRepository.Get(input.Id);

    // Update properties
    entity.SomeProperty = "Updated value";
    entity.SuperMaster.Id = updateInput.SuperMaster.Id;

    foreach (var child in input.Children)
    {
        if (entity.Children.All(x => x.Id != child.Id))
        {
            // This input child doesn't exist in entity.Children -- add it
            // Mapper.Map uses AutoMapper to map from the input DTO to entity
            entity.Children.Add(Mapper.Map<Child>(child));
            continue;
        }

        // The input child exists in entity.Children -- update it
        var oldChild = entity.Children.FirstOrDefault(x => x.Id == child.Id);
        if (oldChild == null)
        {
            continue;
        }

        // The mapper will also update child.RelatedEntity.Id
        Mapper.Map(child, oldChild);
    }

    foreach (var child in entity.Children.Where(x => x.Id != 0).ToList())
    {
        if (input.Children.All(x => x.Id != child.Id))
        {
            // The child doesn't exist in input anymore, mark it for deletion
            child.Id = -1;
        }
    }

    entity = await _masterRepository.UpdateAsync(entity);

    // Use AutoMapper to map from entity to DTO
    return MapToEntityDto(entity);
}

現在存儲庫方法(MasterRepository):

public async Task<Master> UpdateAsync(Master entity)
{
    var superMasterId = entity.SuperMaster.Id;

    // Make sure SuperMaster properties are updated in case the superMasterId is changed
    entity.SuperMaster = await Context.SuperMasters
        .FirstOrDefaultAsync(x => x.Id == superMasterId);

    // New and updated children, skip deleted
    foreach (var child in entity.Children.Where(x => x.Id != -1))
    {
        await _childRepo.InsertOrUpdateAsync(child);
    }

    // Handle deleted children
    foreach (var child in entity.Children.Where(x => x.Id == -1))
    {
        await _childRepo.DeleteAsync(child);
        entity.Children.Remove(child);
    }

    return entity;
}

最后,來自 ChildrenRepository 的相關代碼:

public async Task<Child> InsertOrUpdateAsync(Child entity)
{
    if (entity.Id == 0)
    {
        return await InsertAsync(entity, parent);
    }

    var relatedId = entity.RelatedEntity.Id;
    entity.RelatedEntity = await Context.RelatedEntities
        .FirstOrDefaultAsync(x => x.Id == relatedId);

    // We have already updated child properties in the controller method 
    // and it's expected that changed entities are marked as changed in EF change tracker
    return entity;
}

public async Task<Child> InsertAsync(Child entity)
{
    var relatedId = entity.RelatedEntity.Id;
    entity.RelatedEntity = await Context.RelatedEntities
        .FirstOrDefaultAsync(x => x.Id == relatedId);

    entity = Context.Set<Child>().Add(entity).Entity;

    // We need the entity Id, hence the call to SaveChanges
    await Context.SaveChangesAsync();
    return entity;
}

Context屬性實際上是DbContext並且事務是在操作過濾器中啟動的。 如果操作引發異常,則操作過濾器執行回滾,如果沒有,則調用 SaveChanges。

發送的輸入對象如下所示:

{
  "someProperty": "Some property",
  "superMaster": {
     "name": "SuperMaster name",
     "id": 1
  },
  "children": [
  {
    "relatedEntity": {
      "name": "RelatedEntity name",
      "someOtherProp": 20,
      "id": 1
    },
    "count": 20,
    "someDescription": "Something"
  }],
  "id": 10
}

Masters表當前有一個 ID 為 10 的記錄。它沒有子項。

拋出的異常是:

Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException: Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded.

這是怎么回事? 我認為 EF 應該跟蹤更改,這包括知道我們在該內部方法中調用了 SaveChanges。

編輯刪除對 SaveChanges 的調用不會改變任何內容。 此外,在查看 SQL Server Profiler 中發生的情況時,我找不到 EF 生成的任何 INSERT 或 UPDATE SQL 語句。

EDIT2 INSERT 語句在調用 SaveChanges 時存在,但仍然沒有 Master 實體的 UPDATE 語句。

像往常一樣,將此問題發布到 StackOverflow 幫助我解決了問題。 代碼最初看起來不像上面的問題,但我在編寫問題時更喜歡修復代碼。

在寫這個問題之前,我花了將近一天的時間試圖找出問題所在,所以我嘗試了不同的方法,例如重新創建實體實例並手動附加它們,將某些實體標記為未更改/已修改,使用 AsNoTracking 甚至完全禁用自動更改跟蹤所有實體並手動標記所有實體已添加或修改。

原來導致這個問題的代碼是在那個子存儲庫的私有方法中,我省略了它,因為我認為它不相關。 不過,如果我沒有忘記從中刪除一些手動更改跟蹤代碼,它確實不相關,這基本上擺弄了 EF 的自動更改跟蹤器並導致其行為不端。

但是,多虧了 StackOverflow,問題才得以解決。 當您與某人談論問題時,您需要自己重新分析它,以便能夠解釋它的所有細節,以便與您交談的人(在這種情況下,SO 社區)理解它。 當您重新分析它時,您會注意到所有引起問題的小部分,然后更容易診斷問題。

所以無論如何,如果有人因為標題而被這個問題所吸引,通過谷歌搜索或 w/e,這里有一些關鍵點:

  • 如果您在多個級別更新實體,請始終調用.Include以在獲取現有實體時包含所有相關導航屬性。 這將使它們全部加載到更改跟蹤器中,您無需手動附加/標記。 完成更新后,調用 SaveChanges 將正確保存所有更改。

  • 當您需要更新子實體時,不要將 AutoMapper 用於頂級實體,尤其是當您在更新子實體時必須實現一些額外的邏輯時。

  • 永遠不要像我在將 Id 設置為 -1 時嘗試的那樣更新主鍵,或者像我在控制器 Update 方法中的這一行上嘗試的那樣:

    // The mapper will also update child.RelatedEntity.Id Mapper.Map(child, oldChild);

  • 如果您需要處理已刪除的項目,最好檢測它們並將其存儲在單獨的列表中,然后為每個項目手動調用存儲庫刪除方法,其中存儲庫刪除方法將包含有關相關實體的一些最終附加邏輯。

  • 如果您需要更改相關實體的主鍵,您需要先從關系中刪除該相關實體,然后添加一個具有更新鍵的新實體。

所以這里是更新的控制器操作,省略了空值和安全檢查:

public async Task<OutputDto> Update(InputDto input)
{
    // First get a real entity by Id from the repository
    // This repository method returns: 
    // Context.Masters
    //    .Include(x => x.SuperMaster)
    //    .Include(x => x.Children)
    //    .ThenInclude(x => x.RelatedEntity)
    //    .FirstOrDefault(x => x.Id == id)
    Master entity = await _masterRepository.Get(input.Id);

    // Update the master entity properties manually
    entity.SomeProperty = "Updated value";

    // Prepare a list for any children with modified RelatedEntity
    var changedChildren = new List<Child>();

    foreach (var child in input.Children)
    {
        // Check to see if this is a new child item
        if (entity.Children.All(x => x.Id != child.Id))
        {
            // Map the DTO to child entity and add it to the collection
            entity.Children.Add(Mapper.Map<Child>(child));
            continue;
        }

        // Check to see if this is an existing child item
        var existingChild = entity.Children.FirstOrDefault(x => x.Id == child.Id);
        if (existingChild == null)
        {
            continue;
        }

        // Check to see if the related entity was changed
        if (existingChild.RelatedEntity.Id != child.RelatedEntity.Id)
        {
            // It was changed, add it to changedChildren list
            changedChildren.Add(existingChild);
            continue;
        }

        // It's safe to use AutoMapper to map the child entity and avoid updating properties manually, 
        // provided that it doesn't have child-items of their own
        Mapper.Map(child, existingChild);
    }

    // Find which of the child entities should be deleted
    // entity.IsTransient() is an extension method which returns true if the entity has just been added
    foreach (var child in entity.Children.Where(x => !x.IsTransient()).ToList())
    {
        if (input.Children.Any(x => x.Id == child.Id))
        {
            continue;
        }

        // We don't have this entity in the list sent by the client.
        // That means we should delete it
        await _childRepository.DeleteAsync(child);
        entity.Children.Remove(child);
    }

    // Parse children entities with modified related entities
    foreach (var child in changedChildren)
    {
        var newChild = input.Children.FirstOrDefault(x => x.Id == child.Id);

        // Delete the existing one
        await _childRepository.DeleteAsync(child);
        entity.Children.Remove(child);

        // Add the new one
        // It's OK to change the primary key here, as this one is a DTO, not a tracked entity,
        // and besides, if the keys are autogenerated by the database, we can't have anything but 0 for a new entity
        newChild.Id = 0;
        entity.Djelovi.Add(Mapper.Map<Child>(newChild)); 
    }

    // And finally, call the repository update and return the result mapped to DTO
    entity = await _repository.UpdateAsync(entity);
    return MapToEntityDto(entity);
}

使用這個通用子標記子狀態,使用方便

注意事項:

  • PromatCon:實體對象
  • amList:是您要添加或修改的子列表
  • rList:是要刪除的子列表
updatechild(objCas.ECC_Decision, PromatCon.ECC_Decision.Where(c => c.rid == objCas.rid & !objCas.ECC_Decision.Select(x => x.dcid).Contains(c.dcid)).toList())
public void updatechild<Ety>(ICollection<Ety> amList, ICollection<Ety> rList)
{
        foreach (var obj in amList)
        {
            var x = PromatCon.Entry(obj).GetDatabaseValues();
            if (x == null)
                PromatCon.Entry(obj).State = EntityState.Added;
            else
                PromatCon.Entry(obj).State = EntityState.Modified;
        }
        foreach (var obj in rList.ToList())
            PromatCon.Entry(obj).State = EntityState.Deleted;
}
PromatCon.SaveChanges()

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM