26

郁闷,这个。这是由数据库优先实体框架生成的一对相关对象:

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();
        }

对 Type 的更改会保存,但对 ChargeableRate 的更改不会。我不认为它有很大帮助,但我想我会添加它。

4

7 回答 7

23

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

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

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

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

评论后编辑

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

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

例如MSDN - 使用自我跟踪实体

于 2013-08-05T13:09:16.890 回答
5

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

由于我不知道您如何检索您的案例中的对象,我不确定这是否与您的案例相关,但将其放在那里以防万一。

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

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

于 2013-08-05T11:34:37.533 回答
5

Context.Entry()已经在内部“附加”了实体,以便让上下文更改其EntityState.

通过调用Attach(),您将EntityState返回到Unchanged. 尝试注释掉这一行。

于 2013-08-05T09:47:31.593 回答
2

Graphdiff库对我处理所有这些复杂有很大帮助。

您只需要设置您希望插入/更新/删除的导航属性(使用流畅的语法),Graphdiff 会处理它

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

于 2016-04-15T23:05:50.327 回答
1

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

例如...而不是:

myObject.myProperty = anotherPropertyObject;

尝试这个:

myObject.myPropertyID = anotherPropertyObject.ID;

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

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

祝你好运!:)

于 2014-04-07T20:37:06.713 回答
1

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


考虑一下:

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");
    }
于 2016-12-30T14:22:27.423 回答
1

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

Database.Products.Attach(argProduct);
argProduct.Categories = Database.Categories.Where(x => ListCategories.Contains(x.CategoryId)).ToList();
Database.Entry(argProduct).State = EntityState.Modified;
Database.SaveChanges();
于 2016-09-29T20:19:37.257 回答