实体框架不保存修改后的子项

时间:2013-08-05 09:31:12

标签: c# entity-framework

令人沮丧,这个。这是一对相关对象,由数据库优先实体框架生成:

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的代码 - 它涉及到因为我们将实体对象与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的更改不会。我认为这不大有帮助,但我认为我会添加它。

7 个答案:

答案 0 :(得分:22)

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

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

解决方法 - 这不是最佳做法 - 将迭代当前DefaultCharge的所有DevelopmentType并将实体状态设置为EntityState.Modified

此外,我建议首先将实体附加到上下文,然后更改状态。

评论后编辑

当您使用DTO时,我想您正在通过不同的层或不同的机器传输这些对象。

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

e.g。 MSDN - Working with Self-Tracking Entities

答案 1 :(得分:5)

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

通过致电Attach(),您将EntityState更改回Unchanged。试着注释掉这一行。

答案 2 :(得分:3)

据我所知,只有在使用尝试保存它的相同Context检索父对象时,EF才能保存子实体。这是将一个上下文检索到的对象附加到另一个上下文,允许您保存对父对象而不是子对象的更改。这是我们切换到NHibernate的旧搜索的结果。如果内存正常运行,我能够找到EF团队成员确认此链接的链接,并且没有计划更改此行为。不幸的是,所有与该搜索相关的链接都已从我的PC中删除。

由于我不知道你是如何检索你的案件中的对象,我不确定这是否与你的情况有关,而是把它放在那里以防它有用。

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

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

答案 3 :(得分:2)

Graphdiff库帮助我处理所有这些复杂问题。

您只需要设置要插入/更新/删除的导航属性(使用流利的语法),Graphdiff将负责处理

注意:似乎该项目不再更新,但我已经使用它超过一年并且非常稳定

答案 4 :(得分:0)

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

例如......而不是:

myObject.myProperty = anotherPropertyObject;

试试这个:

myObject.myPropertyID = anotherPropertyObject.ID;

确保对象在EF的脑海中被标记为已修改(如其他帖子中所述),然后调用您的保存方法。

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

祝你好运! :)

答案 5 :(得分:0)

如果我正确理解了这个问题,那么更新子字段时会遇到问题。我遇到了子集合字段的问题。我试过这个,它对我有用。 将对象附加到数据库上下文后,应更新所有子集合,更改父对象的已修改状态,并将更改保存到上下文中。

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

答案 6 :(得分: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");
    }