递归实体更新
本文关键字:更新 实体 递归 | 更新日期: 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();