这两个实体是一对多关系(由代码第一个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?
}
目前我有两个想法:
获取一个被跟踪的父实体,命名为按模型存在的。Id,并将模型中的值逐个分配给实体。这听起来很愚蠢。在模型中。我不知道哪个子是新的,哪个子是修改的(甚至是删除的)。
通过模型创建一个新的父实体,并将其附加到DbContext并保存。但是DbContext如何知道子节点的状态(新增/删除/修改)呢?
实现这个功能的正确方法是什么?
因为我讨厌重复复杂的逻辑,这里有一个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);
}
这不是最优雅的方法,但很有效。干杯!
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));
}
}
因为我讨厌重复复杂的逻辑,这里有一个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);
}
如果你正在使用EntityFrameworkCore,你可以在你的控制器post动作中做以下事情(Attach方法递归地附加导航属性,包括集合):
_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数据中提供。不会为实体的部分更新工作)。
您还需要确保为该操作使用了一个新的/专用的实体框架数据库上下文。
我一直在摆弄这样的东西……
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。
不知道这将如何大规模地执行,但不确定还能做什么来解决这个问题。