Entity Framework не сохраняет измененных детей
Разочарование, это. Здесь пара связанных объектов, созданных базой данных 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 - он включает 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 показывает, что команды обновления выдаются только для родительского объекта, а не для всех подключенных объектов.
У меня есть другой аналогичный код в другом месте, который функционирует так, как ожидалось. Что я здесь делаю неправильно?
EDIT:
Я обнаружил, что если вы сделаете это до вызова SaveDevelopmentType:
using (TransactionScope scope = new TransactionScope())
{
dt.Type = "Test1";
dt.DefaultCharges.First().ChargeableRate = 99;
_CILRepository.SaveDevelopmentType(dt);
scope.Complete();
}
Изменение типа сохраняет, но изменения в ChargeableRate нет. Я не думаю, что это помогает, в массовом порядке, но я думал, что добавлю его.
Ответы
Ответ 1
Проблема заключается в том, что EF не знает об изменениях DefaultCharges.
Установив состояние DevelopmentType
на EntityState.Modified
, EF знает только, что объект DevelopmentType
был изменен. Однако это означает, что EF будет обновлять только DevelopmentType
, но не навигационные свойства.
Обходной путь - это не самая лучшая практика - это перебрать все DefaultCharge
текущего DevelopmentType
и установить состояние объекта EntityState.Modified
.
Кроме того, я бы рекомендовал сначала привязать объект к контексту и после этого изменить состояние.
ИЗМЕНИТЬ после комментария
Поскольку вы используете DTO, я предполагаю, что вы переносите эти объекты либо через разные уровни, либо на разные машины.
В этом случае я бы рекомендовал использовать объекты самостоятельного отслеживания, потому что невозможно разделить один контекст. Эти сущности дополнительно содержат свое текущее состояние (то есть новое, обновленное, удаленное и т.д.). В сети много обучающих программ, посвященных объектам самостоятельного отслеживания.
например. MSDN - Работа с объектами самопроверки
Ответ 2
Context.Entry()
уже "привязывает" Entity внутри, чтобы изменить контекст EntityState
.
Вызывая Attach()
, вы меняете EntityState
на Unchanged
. Попробуйте прокомментировать эту строку.
Ответ 3
Насколько я знаю, EF может сохранять дочерние сущности только в том случае, если родительский объект был восстановлен с тем же Контекстом, который пытается его сохранить. Это привязка объекта, который был получен одним контекстом в другом контексте, позволит вам сохранять изменения в родительских объектах, но не для детей. Это было результатом старого поиска, на основе которого мы перешли на NHibernate. Если память правильно работает, мне удалось найти ссылку, в которой члены (члены) EF подтвердили это, и что там не было никакого плана изменить это поведение. К сожалению, все ссылки, связанные с этим поиском, были удалены с моего ПК с тех пор.
Поскольку я не знаю, как вы извлекаете объекты в своем случае, я не уверен, что это имеет отношение к вашему делу, но поместите его там на всякий случай, если это поможет.
Вот ссылка на прикрепление отдельных объектов к контексту.
http://www.codeproject.com/Articles/576330/Attaching-detached-POCO-to-EF-DbContext-simple-and
Ответ 4
Библиотека Graphdiff очень помогла мне справиться со всеми этими сложностями.
Вам нужно только настроить свойства навигации, которые вы хотите вставить/обновить/удалить (используя свободный синтаксис), и Graphdiff позаботится об этом
Примечание. Кажется, что проект больше не обновляется, но я использую его с более чем года и довольно стабилен.
Ответ 5
Это не обходной путь для каждого случая, но я обнаружил, что вы можете обойти это, обновив внешние ключи для объекта, вместо обновления объектов свойств навигации.
Например... вместо:
myObject.myProperty = anotherPropertyObject;
Попробуйте следующее:
myObject.myPropertyID = anotherPropertyObject.ID;
Убедитесь, что объект отмечен как измененный в умении EF (как указано в других сообщениях), а затем вызовите метод сохранения.
Работал для меня хотя бы! При работе с вложенными свойствами это будет нехорошо, но, возможно, вы можете разбить свои контексты на более мелкие куски и работать над объектами в нескольких частях, чтобы избежать раздувания контекста.
Удачи!:)
Ответ 6
Если я правильно понял вопрос, у вас возникли проблемы с обновлением дочерних полей. У меня были проблемы с полями сбора детей. Я попробовал это, и это сработало для меня.
Вы должны обновить все дочерние коллекции после присоединения объекта к контексту базы данных, изменить измененное состояние родительского объекта и сохранить изменения в контексте.
Database.Products.Attach(argProduct);
argProduct.Categories = Database.Categories.Where(x => ListCategories.Contains(x.CategoryId)).ToList();
Database.Entry(argProduct).State = EntityState.Modified;
Database.SaveChanges();
Ответ 7
Я создал вспомогательный метод для решения этой проблемы.
Рассмотрим это:
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");
}