2013-08-05 26 views
23

令人沮丧,这。这里有一对相关的对象,如数据库,第一个实体框架生成:实体框架未保存修改后的孩子

public partial class DevelopmentType 
{ 
    public DevelopmentType() 
    { 
     this.DefaultCharges = new HashSet<DefaultCharge>(); 
    } 

    public System.Guid RowId { get; set; } 
    public string Type { get; set; } 

    public virtual ICollection<DefaultCharge> DefaultCharges { get; set; } 
} 

public partial class DefaultCharge 
{ 
    public System.Guid RowId { get; set; } 
    public decimal ChargeableRate { get; set; } 
    public Nullable<System.Guid> DevelopmentType_RowId { get; set; } 

    public virtual DevelopmentType DevelopmentType { get; set; } 
} 

下面是我打电话保存DevelopmentType代码 - 它涉及automapper因为我们区分实体从DTO的对象:

public void SaveDevelopmentType(DevelopmentType_dto dt) 
    { 
     Entities.DevelopmentType mappedDevType = Mapper.Map<DevelopmentType_dto, Entities.DevelopmentType>(dt); 
     _Context.Entry(mappedDevType).State = System.Data.EntityState.Modified; 

     _Context.DevelopmentTypes.Attach(mappedDevType); 
     _Context.SaveChanges(); 
    } 

在我的用户界面中,最常见的操作是让用户查看DevelopmentTypes列表并更新其DefaultCharge。所以当我使用上面的代码测试它时,它运行没有错误,但实际上并没有改变。

如果我在调试器中暂停,很明显,已更改的DefaultCharge被传递给该函数,并且它被附加到要保存的DevelopmentType。

单步通过它,如果我在visual studio中手动更改值,它确实保存更新后的值。这更令人困惑。

监控与SQL Server Profiler中的数据库显示,更新命令只签发父对象和任何附加的对象。

我在其他地方有类似的代码,按照预期发挥作用。我在这里做错了什么?

编辑:

我发现,如果你这样做调用SaveDevelopmentType之前:

 using (TransactionScope scope = new TransactionScope()) 
     { 
      dt.Type = "Test1"; 
      dt.DefaultCharges.First().ChargeableRate = 99; 
      _CILRepository.SaveDevelopmentType(dt); 
      scope.Complete(); 
     } 

键入变化扑救,但改变ChargeableRate没有。我认为这不是很有帮助,但我认为我会添加它。

回答

21

的问题是,EF不知道改变的DefaultCharges的。

通过将DevelopmentType的状态设置为EntityState.Modified,EF仅知道对象DevelopmentType已更改。但是,这意味着EF将只更新DevelopmentType,但不是导航属性。

解决方法 - 这不是最佳实践 - 将重复遍历当前DevelopmentType的所有DefaultCharge并将实体状态设置为EntityState.Modified

此外,我会建议先将实体附加到上下文,然后更改状态。当您使用DTO的,我想你要么通过不同层或不同的机器transfering这些对象评论

编辑。

在这种情况下,我会建议使用自我跟踪实体,因为它不可能共享一个上下文。这些实体还保存其当前状态(即新的,更新的,删除的等)。网上有很多关于自我跟踪实体的教程。

例如MSDN - Working with Self-Tracking Entities

+1

这是有效的,并且是对问题的最简洁的解释。谢谢。我宁愿不迭代子对象并每次检查它们的状态,我可以看到为什么它不是最佳实践。那么有什么选择,什么是最佳实践? –

+0

我已经更新了我的回答 – boindiil

+0

都不错,但值得指出objectContext上的STE和DBContext中的新API之间的区别。它的出现是因为大多数新的EF编码器都使用Dbcontext。无论如何,incase有人来看http://msdn.microsoft.com/en-us/data/jj556205 –

5

Context.Entry()已在内部“附加”该实体以使上下文更改其EntityState

致电Attach()您正在更改EntityState回到Unchanged。尝试注释掉这一行。

+0

感谢您的建议,但可悲的是它没有区别。在更改EntityState之前,我已经尝试了附加功能,但这也不起作用。 –

+0

'mappedDevType'是否包含正确的'RowId'? 'RowId'是否映射为实体的主键? – haim770

+0

是的,这都是正确的。在调试器中,对象与预期明确相关(即,DefaultCharges是DevelopmenType上的一个集合,并且它被填充了期望值)。 –

3

据我所知,EF只有在父对象被试图保存的同一个上下文中检索时才能保存子实体。这是将由一个上下文检索到的对象附加到另一个上下文,将允许您将更改保存到父对象而不是子对象。这是基于我们切换到NHibernate的旧搜索的结果。如果内存服务正常,我可以找到一个EF小组成员证实这一点的链接,并且没有计划改变这种行为。不幸的是,所有与该搜索相关的链接都已从我的个人电脑中删除。

因为我不知道你是如何检索你的案例中的对象,我不知道这与你的案件有关,但它把它放在那里,以防万一它帮助。

这是将分离对象附加到上下文的链接。

http://www.codeproject.com/Articles/576330/Attaching-detached-POCO-to-EF-DbContext-simple-and

+0

非常感谢。两端都是相同的环境。通过实现链接文章的一个版本(没有界面)我得到它的工作,但我仍然没有看到*为什么*。 –

0

这不是每种情况下的解决方法,但我确实发现可以通过更新对象上的外键而不是更新导航属性对象来解决此问题。

例如...而不是:

myObject.myProperty = anotherPropertyObject; 

试试这个:

myObject.myPropertyID = anotherPropertyObject.ID; 

请确保该对象被标记为EF的心灵修改(如在其他帖子提到的),然后调用你的保存方法。

至少为我工作!使用嵌套属性时,这将是一个不可行的过程,但是也许可以将上下文分解为更小的块并处理多个部分中的对象以避免上下文膨胀。

祝你好运! :)

2

对于我来说,Graphdiff库帮助我处理所有这些复杂问题。

你只需要设置你希望插入/更新的导航性能/ delete键(使用流利的语法)和Graphdiff会照顾它

注:这似乎是该项目未更新不过,我使用它已超过一年,是非常稳定的

0

如果我正确理解问题,您有问题更新子字段。我在儿童收集领域遇到了问题。我试过这个,它为我工作。 将对象附加到数据库上下文后,应更新所有子集合,更改父对象的修改状态并将更改保存到上下文。

Database.Products.Attach(argProduct); 
argProduct.Categories = Database.Categories.Where(x => ListCategories.Contains(x.CategoryId)).ToList(); 
Database.Entry(argProduct).State = EntityState.Modified; 
Database.SaveChanges(); 
0

我创建了一个帮助器方法来解决这个问题。


考虑一下:

public abstract class BaseEntity 
{ 
    /// <summary> 
    /// The unique identifier for this BaseEntity. 
    /// </summary> 
    [Key]   
    public Guid Id { get; set; } 
} 

public class BaseEntityComparer : IEqualityComparer<BaseEntity> 
{ 
    public bool Equals(BaseEntity left, BaseEntity right) 
    { 
     if (ReferenceEquals(null, right)) { return false; } 
     return ReferenceEquals(left, right) || left.Id.Equals(right.Id); 
    } 

    public int GetHashCode(BaseEntity obj) 
    { 
     return obj.Id.GetHashCode(); 
    } 
} 

public class Event : BaseEntity 
{ 
    [Required(AllowEmptyStrings = false)] 
    [StringLength(256)] 
    public string Name { get; set; } 
    public HashSet<Manager> Managers { get; set; } 
} 

public class Manager : BaseEntity 
{ 
    [Required(AllowEmptyStrings = false)] 
    [StringLength(256)] 
    public string Name { get; set; } 
    public Event Event{ get; set; } 
} 

的DbContext与辅助方法:

public class MyDataContext : DbContext 
{ 
    public MyDataContext() : base("ConnectionName") { } 

    //Tables 
    public DbSet<Event> Events { get; set; } 
    public DbSet<Manager> Managers { get; set; } 

    public async Task AddOrUpdate<T>(T entity, params string[] ignoreProperties) where T : BaseEntity 
    { 
     if (entity == null || Entry(entity).State == EntityState.Added || Entry(entity).State == EntityState.Modified) { return; } 
     var state = await Set<T>().AnyAsync(x => x.Id == entity.Id) ? EntityState.Modified : EntityState.Added; 
     Entry(entity).State = state; 

     var type = typeof(T); 
     RelationshipManager relationship; 
     var stateManager = ((IObjectContextAdapter)this).ObjectContext.ObjectStateManager; 
     if (stateManager.TryGetRelationshipManager(entity, out relationship)) 
     { 
      foreach (var end in relationship.GetAllRelatedEnds()) 
      { 
       var isForeignKey = end.GetType().GetProperty("IsForeignKey", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(end) as bool?; 
       var navigationProperty = end.GetType().GetProperty("NavigationProperty", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(end); 
       var propertyName = navigationProperty?.GetType().GetProperty("Identity", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(navigationProperty) as string; 
       if (string.IsNullOrWhiteSpace(propertyName) || ignoreProperties.Contains(propertyName)) { continue; } 

       var property = type.GetProperty(propertyName); 
       if (property == null) { continue; } 

       if (end is IEnumerable) { await UpdateChildrenInternal(entity, property, isForeignKey == true); } 
       else { await AddOrUpdateInternal(entity, property, ignoreProperties); } 
      } 
     } 

     if (state == EntityState.Modified) 
     { 
      Entry(entity).OriginalValues.SetValues(await Entry(entity).GetDatabaseValuesAsync()); 
      Entry(entity).State = GetChangedProperties(Entry(entity)).Any() ? state : EntityState.Unchanged; 
     } 
    } 

    private async Task AddOrUpdateInternal<T>(T entity, PropertyInfo property, params string[] ignoreProperties) 
    { 
     var method = typeof(EasementDataContext).GetMethod("AddOrUpdate"); 
     var generic = method.MakeGenericMethod(property.PropertyType); 
     await (Task)generic.Invoke(this, new[] { property.GetValue(entity), ignoreProperties }); 
    } 

    private async Task UpdateChildrenInternal<T>(T entity, PropertyInfo property, bool isForeignKey) 
    { 
     var type = typeof(T); 
     var method = isForeignKey ? typeof(EasementDataContext).GetMethod("UpdateForeignChildren") : typeof(EasementDataContext).GetMethod("UpdateChildren"); 
     var objType = property.PropertyType.GetGenericArguments()[0]; 
     var enumerable = typeof(IEnumerable<>).MakeGenericType(objType); 

     var param = Expression.Parameter(type, "x"); 
     var body = Expression.Property(param, property); 
     var lambda = Expression.Lambda(Expression.Convert(body, enumerable), property.Name, new[] { param }); 
     var generic = method.MakeGenericMethod(type, objType); 

     await (Task)generic.Invoke(this, new object[] { entity, lambda, null }); 
    } 

    public async Task UpdateForeignChildren<T, TProperty>(T parent, Expression<Func<T, IEnumerable<TProperty>>> childSelector, IEqualityComparer<TProperty> comparer = null) where T : BaseEntity where TProperty : BaseEntity 
    { 
     var children = (childSelector.Invoke(parent) ?? Enumerable.Empty<TProperty>()).ToList(); 
     foreach (var child in children) { await AddOrUpdate(child); } 

     var existingChildren = await Set<T>().Where(x => x.Id == parent.Id).SelectMany(childSelector).AsNoTracking().ToListAsync(); 

     if (comparer == null) { comparer = new BaseEntityComparer(); } 
     foreach (var child in existingChildren.Except(children, comparer)) { Entry(child).State = EntityState.Deleted; } 
    } 

    public async Task UpdateChildren<T, TProperty>(T parent, Expression<Func<T, IEnumerable<TProperty>>> childSelector, IEqualityComparer<TProperty> comparer = null) where T : BaseEntity where TProperty : BaseEntity 
    { 
     var stateManager = ((IObjectContextAdapter)this).ObjectContext.ObjectStateManager; 
     var currentChildren = childSelector.Invoke(parent) ?? Enumerable.Empty<TProperty>(); 
     var existingChildren = await Set<T>().Where(x => x.Id == parent.Id).SelectMany(childSelector).AsNoTracking().ToListAsync(); 

     if (comparer == null) { comparer = new BaseEntityComparer(); } 
     var addedChildren = currentChildren.Except(existingChildren, comparer).AsEnumerable(); 
     var deletedChildren = existingChildren.Except(currentChildren, comparer).AsEnumerable(); 

     foreach (var child in currentChildren) { await AddOrUpdate(child); } 
     foreach (var child in addedChildren) { stateManager.ChangeRelationshipState(parent, child, childSelector.Name, EntityState.Added); } 
     foreach (var child in deletedChildren) 
     { 
      Entry(child).State = EntityState.Unchanged; 
      stateManager.ChangeRelationshipState(parent, child, childSelector.Name, EntityState.Deleted); 
     } 
    } 

    public static IEnumerable<string> GetChangedProperties(DbEntityEntry dbEntry) 
    { 
     var propertyNames = dbEntry.State == EntityState.Added ? dbEntry.CurrentValues.PropertyNames : dbEntry.OriginalValues.PropertyNames; 
     foreach (var propertyName in propertyNames) 
     { 
      if (IsValueChanged(dbEntry, propertyName)) 
      { 
       yield return propertyName; 
      } 
     } 
    } 

    private static bool IsValueChanged(DbEntityEntry dbEntry, string propertyName) 
    { 
     return !Equals(OriginalValue(dbEntry, propertyName), CurrentValue(dbEntry, propertyName)); 
    } 

    private static string OriginalValue(DbEntityEntry dbEntry, string propertyName) 
    { 
     string originalValue = null; 

     if (dbEntry.State == EntityState.Modified) 
     { 
      originalValue = dbEntry.OriginalValues.GetValue<object>(propertyName) == null 
       ? null 
       : dbEntry.OriginalValues.GetValue<object>(propertyName).ToString(); 
     } 

     return originalValue; 
    } 

    private static string CurrentValue(DbEntityEntry dbEntry, string propertyName) 
    { 
     string newValue; 

     try 
     { 
      newValue = dbEntry.CurrentValues.GetValue<object>(propertyName) == null 
       ? null 
       : dbEntry.CurrentValues.GetValue<object>(propertyName).ToString(); 
     } 
     catch (InvalidOperationException) // It will be invalid operation when its in deleted state. in that case, new value should be null 
     { 
      newValue = null; 
     } 

     return newValue; 
    } 
} 

然后我这样称呼它

// POST: Admin/Events/Edit/5 
    [HttpPost] 
    [ValidateAntiForgeryToken] 
    public async Task<ActionResult> Edit(Event @event) 
    { 
     if (!ModelState.IsValid) { return View(@event); } 

     await _db.AddOrUpdate(@event); 
     await _db.SaveChangesAsync(); 

     return RedirectToAction("Index"); 
    } 
+0

这使用反射来尝试并找到所有更改并更新上下文。 – Bryan

+0

它可能会使不必要的调用对象没有改变。我会在稍后处理。 – Bryan