递归实体更新

本文关键字:更新 实体 递归 | 更新日期: 2023-09-27 18:25:30

我有一个实体,它包含一个实体列表(与根实体相同)来表示文件夹结构:

public class SopFolder
{
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTime? LastUpdated { get; set; }
    public int Status { get; set; }
    public virtual ICollection<SopField> SopFields { get; set; }
    public virtual ICollection<SopFolder> SopFolderChildrens { get; set; }
    public virtual ICollection<SopBlock> Blocks { get; set; }
    public virtual ICollection<SopReview> Reviews { get; set; }
}

这个实体使用代码优先的方法存储在我的数据库中,运行良好。然后,我将实体打印到KendoUI树视图中,让用户修改它,并在"保存"时将它作为IEnumerable<TreeViewItemModel> items发布回服务器的Action。

然后,我查找ROOT实体及其所有子实体(只有一个根),并将其转换回SopFolder对象。

为了在数据库中更新完整的对象,我做了以下操作:

 List<SopFolder> sopfolderlist = ConvertTree(items.First());
        SopFolder sopfolder = sopfolderlist[0];
        if (ModelState.IsValid)
        {
            SopFolder startFolder = new SopFolder { Id = sopfolder.Id };
            //db.SopFolders.Attach(startFolder);
           // db.SopFolders.Attach(sopfolder);
            startFolder.Name = sopfolder.Name;
            startFolder.LastUpdated = sopfolder.LastUpdated;
            startFolder.SopFields = sopfolder.SopFields;
            startFolder.SopFolderChildrens = sopfolder.SopFolderChildrens;
            startFolder.Status = sopfolder.Status;
            db.Entry(startFolder).State = EntityState.Modified;
            db.SaveChanges();
            return Content("true");
        }

然而,这并不奏效。模型根本没有更新。如果我在修改之前移动"entityState.Modified",它只会在数据库中创建一个完全新的数据副本(当然是修改的)。

我的方法正确吗?还是我必须走另一条路?我在这里错过了什么?我想还有另一个"隐藏"id可以让EF将实体映射到数据库条目,但我对此不确定。谢谢你的帮助!

更新:我没有创建SopFolder的新实例,而是尝试了db.SopFolders.Find(sopfolder.Id),这适用于没有子项的条目。如果我有带子项的实体,它会创建一个重复项。

谨致问候,Marcus

递归实体更新

这是典型的断开连接图场景。有关可能的解决方案,请参阅此问题:更新对象图时实体框架的断开行为

您已经找到了第一个解决方案,即:单独更新实体。实际上,您应该做的是从数据库中获取原始数据,然后对更改的内容进行比较。有一些通用的方法可以做到这一点,其中一些在J.Lerman的《编程EF DbContext》一书中有描述,在使用EF进行更多编码之前,我强烈建议您使用该书。

p.S.IMHO这是外汇基金更糟糕的下行。

替换SopFolder startFolder=new SopFolder{Id=SopFolder.Id};带有

 SopFolder startFolder = db.SopFolders.FirstOrDefault(s=>s.Id.Equals(sopfolder.Id));
 // then validate if startFolder != null

我建议您使用ParentId创建实体模型,而不是子对象列表。当您需要树视图模型时,使用递归函数从数据库中收集它。

public class SopFolder
{
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTime? LastUpdated { get; set; }
    public int Status { get; set; }
    public virtual ICollection<SopField> SopFields { get; set; }
    //public virtual ICollection<SopFolder> SopFolderChildrens { get; set; }
    public int? ParentFolderId { get; set; }
    public virtual ICollection<SopBlock> Blocks { get; set; }
    public virtual ICollection<SopReview> Reviews { get; set; }
}

创建子文件夹时,请选择其父文件夹,以便收集数据。在儿童的情况下,试试这个:

List<SopFolder> sopfolderlist = ConvertTree(items.First());
        SopFolder sopfolder = sopfolderlist[0];
        if (ModelState.IsValid)
        {
            SopFolder startFolder = new SopFolder { Id = sopfolder.Id };
            //db.SopFolders.Attach(startFolder);
           // db.SopFolders.Attach(sopfolder);
            startFolder.Name = sopfolder.Name;
            startFolder.LastUpdated = sopfolder.LastUpdated;
            startFolder.SopFields = sopfolder.SopFields;
            startFolder.SopFolderChildrens = sopfolder.SopFolderChildrens;
            foreach (var child in sopfolder.SopFolderChildrens)
            {
               db.SopFolders.CurrentValues.SetValues(child);
               db.SaveChanges();
            }
            startFolder.Status = sopfolder.Status;
            db.Entry(startFolder).State = EntityState.Modified;
            db.SaveChanges();
            return Content("true");
        }

我提出了这个解决方案,并且效果显著。

    /// <summary>
/// simple update method that will help you to do a full update to an aggregate graph with all related entities in it.
/// the update method will take the loaded aggregate entity from the DB and the passed one that may come from the API layer.
/// the method will update just the eager loaded entities in the aggregate "The included entities"
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="context"></param>
/// <param name="newEntity">The De-Attached Entity</param>
public static void UpdateGraph<T>(this DbContext context, T newEntity) where T : class
{
    var existingEntity = context.Set<T>().FindAttachedEntry(newEntity);
    UpdateGraph(context, newEntity, existingEntity, null);
}
private static T? FindAttachedEntry<T>(this DbSet<T> set, T entity) where T : class
{
    var primaryKeys = set.EntityType.FindPrimaryKey()!.Properties
        .Select(x => new { getter = x.GetGetter(), comparer = x.GetKeyValueComparer() })
        .ToArray();
    return set.Local
        .FirstOrDefault(local => primaryKeys.All(comparer =>
                comparer.comparer.Equals(comparer.getter.GetClrValue(local), comparer.getter.GetClrValue(entity))
            )
        );
}
private static void UpdateGraph<T>(this DbContext context, T? newEntity, T? existingEntity,
    string? parentAggregateTypeName)
    where T : class
{
    if (existingEntity == null && newEntity != null)
    {
        context.Entry(newEntity).State = EntityState.Added;
        return;
    }
    if (newEntity == null && existingEntity != null)
    {
        context.Entry(existingEntity).State = EntityState.Deleted;
        return;
    }
    if (existingEntity is null || newEntity is null)
    {
        throw new UnreachableException();
    }
    var existingEntry = context.Entry(existingEntity);
    existingEntry.CurrentValues.SetValues(newEntity);
    foreach (var navigationEntry in existingEntry.Navigations.Where(n =>
                 n.IsLoaded && n.Metadata.ClrType.FullName != parentAggregateTypeName))
    {
        var entityTypeName = existingEntry.Metadata.ClrType.FullName;
        var newValue = existingEntry.Entity.GetType().GetProperty(navigationEntry.Metadata.Name)
            ?.GetValue(newEntity);
        var existingValue = navigationEntry.CurrentValue;
        //if (navigationEntry.Metadata.IsCollection()) causes Error CS1929  'INavigationBase' does not contain a definition for 'IsCollection' and the best extension method overload 'NavigationExtensions.IsCollection(INavigation)' requires a receiver of type 'INavigation'   
        //use instead https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.changetracking.collectionentry?view=efcore-7.0
        if (navigationEntry is CollectionEntry)
        {
            var newItems = newValue as IEnumerable<object> ?? Array.Empty<object>();
            var existingItems = (existingValue as IEnumerable<object>)?.ToList() ?? new List<object>();
            // get new and updated items
            foreach (var newItem in newItems)
            {
                var key = context.Entry(newItem).GetPrimaryKeyValues();
                var existingItem =
                    existingItems.FirstOrDefault(x => context.Entry(x).GetPrimaryKeyValues().SequenceEqual(key));
                if (existingItem is not null)
                {
                    existingItems.Remove(existingItem);
                }
                UpdateGraph(context, newItem, existingItem, entityTypeName);
            }
            foreach (var existingItem in existingItems)
            {
                UpdateGraph(context, null, existingItem, entityTypeName);
            }
        }
        else
        {
            // the navigation is not a list
            UpdateGraph(context, newValue, existingValue, entityTypeName);
        }
    }
}

这就是我通常使用它的方式:

// fetch the entity from the db and load it into the changes tracker
    var model = _db.Foos.FirstOrDefault(x => x.Id == dto.Id);
    var updatedModel = dto.Map();
    // the update graph method will find the loaded entity and 
    // compare its properties against the updatedModel. If the 
    // model was not loaded (it does not exist) it will perform an
    // insertion instead of an update
    _db.UpdateGraph(updatedModel); 
    await _db.SaveChangesAsync();