在EF中更新父实体时如何添加/更新子实体

这两个实体是一对多关系(由代码第一个fluent api构建)。

public class Parent
{
public Parent()
{
this.Children = new List<Child>();
}


public int Id { get; set; }


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


public class Child
{
public int Id { get; set; }


public int ParentId { get; set; }


public string Data { get; set; }
}

在我的WebApi控制器中,我有创建父实体(工作正常)和更新父实体(有一些问题)的操作。更新操作如下所示:

public void Update(UpdateParentModel model)
{
//what should be done here?
}

目前我有两个想法:

  1. 通过model.Id获取一个名为existing的被跟踪父实体,并将model中的值逐个赋给该实体。这听起来很愚蠢。在model.Children中,我不知道哪个子元素是新的,哪个子元素被修改了(甚至删除了)。

  2. 通过model创建一个新的父实体,并将其附加到DbContext并保存。但是DbContext如何知道子节点的状态(新增/删除/修改)呢?

实现这个功能的正确方法是什么?

189338 次浏览

因为发布到WebApi控制器的模型是从任何实体框架(EF)上下文中分离出来的,所以唯一的选择是从数据库中加载对象图(父对象图包括其子对象图),并比较哪些子对象图被添加、删除或更新。(除非你在分离状态下(在浏览器中或其他地方)使用自己的跟踪机制跟踪更改,在我看来这比下面的更复杂。)它可能是这样的:

public void Update(UpdateParentModel model)
{
var existingParent = _dbContext.Parents
.Where(p => p.Id == model.Id)
.Include(p => p.Children)
.SingleOrDefault();


if (existingParent != null)
{
// Update parent
_dbContext.Entry(existingParent).CurrentValues.SetValues(model);


// Delete children
foreach (var existingChild in existingParent.Children.ToList())
{
if (!model.Children.Any(c => c.Id == existingChild.Id))
_dbContext.Children.Remove(existingChild);
}


// Update and Insert children
foreach (var childModel in model.Children)
{
var existingChild = existingParent.Children
.Where(c => c.Id == childModel.Id && c.Id != default(int))
.SingleOrDefault();


if (existingChild != null)
// Update child
_dbContext.Entry(existingChild).CurrentValues.SetValues(childModel);
else
{
// Insert child
var newChild = new Child
{
Data = childModel.Data,
//...
};
existingParent.Children.Add(newChild);
}
}


_dbContext.SaveChanges();
}
}

...CurrentValues.SetValues可以接受任何对象,并根据属性名将属性值映射到附加的实体。如果模型中的属性名称与实体中的名称不同,则不能使用此方法,必须逐个分配值。

我一直在摆弄这样的东西……

protected void UpdateChildCollection<Tparent, Tid , Tchild>(Tparent dbItem, Tparent newItem, Func<Tparent, IEnumerable<Tchild>> selector, Func<Tchild, Tid> idSelector) where Tchild : class
{
var dbItems = selector(dbItem).ToList();
var newItems = selector(newItem).ToList();


if (dbItems == null && newItems == null)
return;


var original = dbItems?.ToDictionary(idSelector) ?? new Dictionary<Tid, Tchild>();
var updated = newItems?.ToDictionary(idSelector) ?? new Dictionary<Tid, Tchild>();


var toRemove = original.Where(i => !updated.ContainsKey(i.Key)).ToArray();
var removed = toRemove.Select(i => DbContext.Entry(i.Value).State = EntityState.Deleted).ToArray();


var toUpdate = original.Where(i => updated.ContainsKey(i.Key)).ToList();
toUpdate.ForEach(i => DbContext.Entry(i.Value).CurrentValues.SetValues(updated[i.Key]));


var toAdd = updated.Where(i => !original.ContainsKey(i.Key)).ToList();
toAdd.ForEach(i => DbContext.Set<Tchild>().Add(i.Value));
}

你可以这样调用:

UpdateChildCollection(dbCopy, detached, p => p.MyCollectionProp, collectionItem => collectionItem.Id)

不幸的是,如果子类型上有集合属性也需要更新,这种方法就会失败。考虑通过传递一个IRepository(带有基本的CRUD方法)来解决这个问题,这个IRepository将负责自己调用UpdateChildCollection。调用repo,而不是直接调用DbContext.Entry。

不知道这将如何大规模地执行,但不确定还能做什么来解决这个问题。

只是概念证明 Controler.UpdateModel不能正确工作。

完整类在这里:

const string PK = "Id";
protected Models.Entities con;
protected System.Data.Entity.DbSet<T> model;


private void TestUpdate(object item)
{
var props = item.GetType().GetProperties();
foreach (var prop in props)
{
object value = prop.GetValue(item);
if (prop.PropertyType.IsInterface && value != null)
{
foreach (var iItem in (System.Collections.IEnumerable)value)
{
TestUpdate(iItem);
}
}
}


int id = (int)item.GetType().GetProperty(PK).GetValue(item);
if (id == 0)
{
con.Entry(item).State = System.Data.Entity.EntityState.Added;
}
else
{
con.Entry(item).State = System.Data.Entity.EntityState.Modified;
}


}

如果你正在使用EntityFrameworkCore,你可以在你的控制器post动作中执行以下操作(连接方法递归地附加导航属性,包括集合):

_context.Attach(modelPostedToController);


IEnumerable<EntityEntry> unchangedEntities = _context.ChangeTracker.Entries().Where(x => x.State == EntityState.Unchanged);


foreach(EntityEntry ee in unchangedEntities){
ee.State = EntityState.Modified;
}


await _context.SaveChangesAsync();

假设每个被更新的实体都设置了所有属性,并在来自客户端的post数据中提供。不会为实体的部分更新工作)。

您还需要确保为该操作使用了一个新的/专用的实体框架数据库上下文。

好男人。我曾经有过这个答案,但后来又失去了。当你知道有更好的方法,但却想不起来或找不到时,绝对是一种折磨!这很简单。我只是用多种方法测试了一下。

var parent = _dbContext.Parents
.Where(p => p.Id == model.Id)
.Include(p => p.Children)
.FirstOrDefault();


parent.Children = _dbContext.Children.Where(c => <Query for New List Here>);
_dbContext.Entry(parent).State = EntityState.Modified;


_dbContext.SaveChanges();

你可以用一个新的列表替换整个列表!SQL代码将根据需要删除和添加实体。你不必为那件事操心。确保包含子集合,否则就没有骰子。好运!

public async Task<IHttpActionResult> PutParent(int id, Parent parent)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}


if (id != parent.Id)
{
return BadRequest();
}


db.Entry(parent).State = EntityState.Modified;


foreach (Child child in parent.Children)
{
db.Entry(child).State = child.Id == 0 ? EntityState.Added : EntityState.Modified;
}


try
{
await db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!ParentExists(id))
{
return NotFound();
}
else
{
throw;
}
}


return Ok(db.Parents.Find(id));
}

这就是我解决这个问题的方法。通过这种方式,EF知道要添加哪些来更新哪些。

VB。NET开发人员使用这个通用子标记子状态,易于使用

注:

  • PromatCon:实体对象
  • amList:要添加或修改的子列表
  • rList:要删除的子列表
updatechild(objCas.ECC_Decision, PromatCon.ECC_Decision.Where(Function(c) c.rid = objCas.rid And Not objCas.ECC_Decision.Select(Function(x) x.dcid).Contains(c.dcid)).toList)
Sub updatechild(Of Ety)(amList As ICollection(Of Ety), rList As ICollection(Of Ety))
If amList IsNot Nothing Then
For Each obj In amList
Dim x = PromatCon.Entry(obj).GetDatabaseValues()
If x Is Nothing Then
PromatCon.Entry(obj).State = EntityState.Added
Else
PromatCon.Entry(obj).State = EntityState.Modified
End If
Next
End If


If rList IsNot Nothing Then
For Each obj In rList.ToList
PromatCon.Entry(obj).State = EntityState.Deleted
Next
End If
End Sub
PromatCon.SaveChanges()

这是我的代码,工作得很好。

public async Task<bool> UpdateDeviceShutdownAsync(Guid id, DateTime shutdownAtTime, int areaID, decimal mileage,
decimal motohours, int driverID, List<int> commission,
string shutdownPlaceDescr, int deviceShutdownTypeID, string deviceShutdownDesc,
bool isTransportation, string violationConditions, DateTime shutdownStartTime,
DateTime shutdownEndTime, string notes, List<Guid> faultIDs )
{
try
{
using (var db = new GJobEntities())
{
var isExisting = await db.DeviceShutdowns.FirstOrDefaultAsync(x => x.ID == id);


if (isExisting != null)
{
isExisting.AreaID = areaID;
isExisting.DriverID = driverID;
isExisting.IsTransportation = isTransportation;
isExisting.Mileage = mileage;
isExisting.Motohours = motohours;
isExisting.Notes = notes;
isExisting.DeviceShutdownDesc = deviceShutdownDesc;
isExisting.DeviceShutdownTypeID = deviceShutdownTypeID;
isExisting.ShutdownAtTime = shutdownAtTime;
isExisting.ShutdownEndTime = shutdownEndTime;
isExisting.ShutdownStartTime = shutdownStartTime;
isExisting.ShutdownPlaceDescr = shutdownPlaceDescr;
isExisting.ViolationConditions = violationConditions;


// Delete children
foreach (var existingChild in isExisting.DeviceShutdownFaults.ToList())
{
db.DeviceShutdownFaults.Remove(existingChild);
}


if (faultIDs != null && faultIDs.Any())
{
foreach (var faultItem in faultIDs)
{
var newChild = new DeviceShutdownFault
{
ID = Guid.NewGuid(),
DDFaultID = faultItem,
DeviceShutdownID = isExisting.ID,
};


isExisting.DeviceShutdownFaults.Add(newChild);
}
}


// Delete all children
foreach (var existingChild in isExisting.DeviceShutdownComissions.ToList())
{
db.DeviceShutdownComissions.Remove(existingChild);
}


// Add all new children
if (commission != null && commission.Any())
{
foreach (var cItem in commission)
{
var newChild = new DeviceShutdownComission
{
ID = Guid.NewGuid(),
PersonalID = cItem,
DeviceShutdownID = isExisting.ID,
};


isExisting.DeviceShutdownComissions.Add(newChild);
}
}


await db.SaveChangesAsync();


return true;
}
}
}
catch (Exception ex)
{
logger.Error(ex);
}


return false;
}

因为我讨厌重复复杂的逻辑,这里有一个Slauma解决方案的通用版本。

这是我的更新方法。请注意,在分离场景中,有时您的代码将读取数据,然后更新它,因此它并不总是分离的。

public async Task UpdateAsync(TempOrder order)
{
order.CheckNotNull(nameof(order));
order.OrderId.CheckNotNull(nameof(order.OrderId));


order.DateModified = _dateService.UtcNow;


if (_context.Entry(order).State == EntityState.Modified)
{
await _context.SaveChangesAsync().ConfigureAwait(false);
}
else // Detached.
{
var existing = await SelectAsync(order.OrderId!.Value).ConfigureAwait(false);
if (existing != null)
{
order.DateModified = _dateService.UtcNow;
_context.TrackChildChanges(order.Products, existing.Products, (a, b) => a.OrderProductId == b.OrderProductId);
await _context.SaveChangesAsync(order, existing).ConfigureAwait(false);
}
}
}

这里定义了CheckNotNull

创建这些扩展方法。

/// <summary>
/// Tracks changes on childs models by comparing with latest database state.
/// </summary>
/// <typeparam name="T">The type of model to track.</typeparam>
/// <param name="context">The database context tracking changes.</param>
/// <param name="childs">The childs to update, detached from the context.</param>
/// <param name="existingChilds">The latest existing data, attached to the context.</param>
/// <param name="match">A function to match models by their primary key(s).</param>
public static void TrackChildChanges<T>(this DbContext context, IList<T> childs, IList<T> existingChilds, Func<T, T, bool> match)
where T : class
{
context.CheckNotNull(nameof(context));
childs.CheckNotNull(nameof(childs));
existingChilds.CheckNotNull(nameof(existingChilds));


// Delete childs.
foreach (var existing in existingChilds.ToList())
{
if (!childs.Any(c => match(c, existing)))
{
existingChilds.Remove(existing);
}
}


// Update and Insert childs.
var existingChildsCopy = existingChilds.ToList();
foreach (var item in childs.ToList())
{
var existing = existingChildsCopy
.Where(c => match(c, item))
.SingleOrDefault();


if (existing != null)
{
// Update child.
context.Entry(existing).CurrentValues.SetValues(item);
}
else
{
// Insert child.
existingChilds.Add(item);
// context.Entry(item).State = EntityState.Added;
}
}
}


/// <summary>
/// Saves changes to a detached model by comparing it with the latest data.
/// </summary>
/// <typeparam name="T">The type of model to save.</typeparam>
/// <param name="context">The database context tracking changes.</param>
/// <param name="model">The model object to save.</param>
/// <param name="existing">The latest model data.</param>
public static void SaveChanges<T>(this DbContext context, T model, T existing)
where T : class
{
context.CheckNotNull(nameof(context));
model.CheckNotNull(nameof(context));


context.Entry(existing).CurrentValues.SetValues(model);
context.SaveChanges();
}


/// <summary>
/// Saves changes to a detached model by comparing it with the latest data.
/// </summary>
/// <typeparam name="T">The type of model to save.</typeparam>
/// <param name="context">The database context tracking changes.</param>
/// <param name="model">The model object to save.</param>
/// <param name="existing">The latest model data.</param>
/// <param name="cancellationToken">A cancellation token to cancel the operation.</param>
/// <returns></returns>
public static async Task SaveChangesAsync<T>(this DbContext context, T model, T existing, CancellationToken cancellationToken = default)
where T : class
{
context.CheckNotNull(nameof(context));
model.CheckNotNull(nameof(context));


context.Entry(existing).CurrentValues.SetValues(model);
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}
参考下面的代码片段从我的一个项目,我实现了同样的事情。它将保存数据,如果有新的条目,更新,如果记录是不可用的张贴json。 Json数据帮助你理解模式:

{
"groupId": 1,
"groupName": "Group 1",
"sortOrder": 1,
"filterNames": [
{
"filterId": 1,
"filterName1": "Name11111",
"sortOrder": 10,
"groupId": 1
}  ,
{
"filterId": 1006,
"filterName1": "Name Changed 1",
"sortOrder": 10,
"groupId": 1
}  ,
{
"filterId": 1007,
"filterName1": "New Filter 1",
"sortOrder": 10,
"groupId": 1
} ,
{
"filterId": 2,
"filterName1": "Name 2 Changed",
"sortOrder": 10,
"groupId": 1
}
]
}




public async Task<int> UpdateFilter(FilterGroup filterGroup)
{
var Ids = from f in filterGroup.FilterNames select f.FilterId;
var toBeDeleted = dbContext.FilterNames.Where(x => x.GroupId == filterGroup.GroupId
&& !Ids.Contains(x.FilterId)).ToList();
foreach(var item in toBeDeleted)
{
dbContext.FilterNames.Remove(item);
}
await dbContext.SaveChangesAsync();


dbContext.FilterGroups.Attach(filterGroup);
dbContext.Entry(filterGroup).State = EntityState.Modified;
for(int i=0;i<filterGroup.FilterNames.Count();i++)
{
if (filterGroup.FilterNames.ElementAt(i).FilterId != 0)
{
dbContext.Entry(filterGroup.FilterNames.ElementAt(i)).State = EntityState.Modified;
}
}
return await dbContext.SaveChangesAsync();
}

这个应该可以了……

private void Reconcile<T>(DbContext context,
IReadOnlyCollection<T> oldItems,
IReadOnlyCollection<T> newItems,
Func<T, T, bool> compare)
{
var itemsToAdd = new List<T>();
var itemsToRemove = new List<T>();


foreach (T newItem in newItems)
{
T oldItem = oldItems.FirstOrDefault(arg1 => compare(arg1, newItem));


if (oldItem == null)
{
itemsToAdd.Add(newItem);
}
else
{
context.Entry(oldItem).CurrentValues.SetValues(newItem);
}
}


foreach (T oldItem in oldItems)
{
if (!newItems.Any(arg1 => compare(arg1, oldItem)))
{
itemsToRemove.Add(oldItem);
}
}


foreach (T item in itemsToAdd)
context.Add(item);


foreach (T item in itemsToRemove)
context.Remove(item);
}

这不是最优雅的方法,但很有效。干杯!

var entity = await context.Entities.FindAsync(id);


var newEntity = new AmazingEntity() {
p1 = child1
p2 = child2
p3 = child3.child4 //... nested collections
};


if (entity != null)
{
db.Entities.Remove(entity);
}


db.Entities.Add(newEntity);


await db.SaveChangesAsync();

记住去掉PK。

var child4 = Tools.CloneJson(deepNestedElement);
child4.id = 0;
child3.Add(child4);




public static class Tools
{
public static JsonSerializerSettings jsonSettings = new JsonSerializerSettings {
ObjectCreationHandling = ObjectCreationHandling.Replace,
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
};


public static string JSerialize<T>(T source) {
return JsonConvert.SerializeObject(source, Formatting.Indented, jsonSettings);
}


public static T JDeserialize<T>(string source) {
return JsonConvert.DeserializeObject<T>(source, jsonSettings);
}


public static T CloneJson<T>(this T source)
{
return CloneJson<T, T>(source);
}


public static TOut CloneJson<TIn, TOut>(TIn source)
{
if (Object.ReferenceEquals(source, null))
return default(TOut);
return JDeserialize<TOut>(JSerialize(source));
}
}
所以,我最终设法让它工作,虽然不是完全自动的。
3.注意AutoMapper <它处理所有属性的映射,因此您不必手动执行。同样,如果用于从一个对象映射到另一个对象的方式,那么它只更新属性,并将更改的属性标记为Modified to EF,这是我们想要的。
如果你使用显式的context.Update(entity),区别在于整个对象将被标记为Modified,并且每个道具将被更新。
在这种情况下,你不需要跟踪,但缺点是如上所述。
也许这对你来说不是问题,但它更昂贵,我想在保存中记录准确的更改,所以我需要正确的信息
            // We always want tracking for auto-updates
var entityToUpdate = unitOfWork.GetGenericRepository<Article, int>()
.GetAllActive() // Uses EF tracking
.Include(e => e.Barcodes.Where(e => e.Status == DatabaseEntityStatus.Active))
.First(e => e.Id == request.Id);


mapper.Map(request, entityToUpdate); // Maps it to entity with AutoMapper <3
ModifyBarcodes(entityToUpdate, request);


// Removed part of the code for space


unitOfWork.Save();

ModifyBarcodes部分这里。
我们希望以一种EF跟踪不会被打乱的方式修改集合。
不幸的是,AutoMapper映射会创建一个全新的集合实例,因此会搞砸跟踪,尽管,我很确定它应该工作。 无论如何,因为我从FE发送完整的列表,在这里我们实际上决定了应该添加/更新/删除什么,只是处理列表本身。
由于EF跟踪是打开的,EF处理它就像一个魅力

            var toUpdate = article.Barcodes
.Where(e => articleDto.Barcodes.Select(b => b.Id).Contains(e.Id))
.ToList();


toUpdate.ForEach(e =>
{
var newValue = articleDto.Barcodes.FirstOrDefault(f => f.Id == e.Id);
mapper.Map(newValue, e);
});


var toAdd = articleDto.Barcodes
.Where(e => !article.Barcodes.Select(b => b.Id).Contains(e.Id))
.Select(e => mapper.Map<Barcode>(e))
.ToList();


article.Barcodes.AddRange(toAdd);


article.Barcodes
.Where(e => !articleDto.Barcodes.Select(b => b.Id).Contains(e.Id))
.ToList()
.ForEach(e => article.Barcodes.Remove(e));




CreateMap<ArticleDto, Article>()
.ForMember(e => e.DateCreated, opt => opt.Ignore())
.ForMember(e => e.DateModified, opt => opt.Ignore())
.ForMember(e => e.CreatedById, opt => opt.Ignore())
.ForMember(e => e.LastModifiedById, opt => opt.Ignore())
.ForMember(e => e.Status, opt => opt.Ignore())
// When mapping collections, the reference itself is destroyed
// hence f* up EF tracking and makes it think all previous is deleted
// Better to leave it on manual and handle collecion manually
.ForMember(e => e.Barcodes, opt => opt.Ignore())
.ReverseMap()
.ForMember(e => e.Barcodes, opt => opt.MapFrom(src => src.Barcodes.Where(e => e.Status == DatabaseEntityStatus.Active)));