Обновление вложенного списка без отслеживания ошибок

#c# #entity-framework #entity-framework-core

#c# #entity-framework #сущность-фреймворк-ядро

Вопрос:

Упрощенная модель:

 Profile {Guid Id, string Name, List<Property> Properties}
Property {Guid Id, string Name, List<Type> Types}
Type {Guid Id, string Key, string Value}
 

DbContext:

 {
     public DbSet<Profile> Profiles { get; set; }
}
 

Я не включил Properties and Types в DbContext, поэтому я использовал ModelBuilder:

 modelBuilder.Entity<Property>().HasMany<Type>();
 

В службе обновления:

 public async Task<Response> Update([FromBody] Profile profile)
{
var entity = await _context.Profiles
    .Include(x => x.Properties)
    .ThenInclude(x => x.Types)
    .FirstOrDefaultAsync(x => x.Id == profile.Id);

foreach (var prop in profile.Properties)
{
    var existingProp = entity.Properties.SingleOrDefault(a => a.Id == prop.Id);
    //Update
    if (existingProp != null)
    {
        var entry = _context.Entry(existingProp);
        entry.State = EntityState.Modified;
        existingProp.ChargeFrom(prop);//maps the new values to the db entity
        _context.SaveChanges();
    }
}
}
 

Но приведенный выше код выдает это исключение в SaveChanges :

Экземпляр объекта типа ‘Type’ не может быть отслежен, поскольку другой экземпляр с тем же значением ключа для {‘Id’} уже отслеживается. При присоединении существующих объектов убедитесь, что присоединен только один экземпляр объекта с заданным значением ключа. Рассмотрите возможность использования DbContextOptionsBuilder.EnableSensitiveDataLogging’ для просмотра конфликтующих значений ключа.

Я пометил Types объект AsNoTracking :

 .ThenInclude(x => x.Types).AsNoTracking()
 

и проблема решена, но я не знаю, почему возникает это исключение, в каком-то другом потоке упоминается, что DbContext может использоваться другим процессом и может быть зарегистрирован как одноэлементный, но в моем случае он зарегистрирован как ограниченный.

Комментарии:

1. Избавьтесь от бесполезной entry переменной и entry.State модификации, и проблема решена. existingProp уже отслеживается EF, вам не нужно вручную добавлять его для отслеживания

2. Value Свойство не обновляется без него, или родительский объект ( entity ) должен быть Modified

Ответ №1:

Я пометил объект Types как отслеживание: .ThenInclude(x => x.Types).AsNoTracking() и проблема решена, но я не знаю, почему возникает это исключение

Причиной ошибки будет то, что эта строка:

 existingProp.ChargeFrom(prop);//maps the new values to the db entity
 

… будет пытаться скопировать неотслеживаемые типы из prop into existingProp . Использование AsNoTracking удалит исключение, но, скорее всего, это приведет к дублированию данных о SaveChanges том, где Type будет настроен идентификационный ключ или повторяющиеся исключения строк. Если вы не получили исключения, я бы проверил коллекцию типов, чтобы увидеть, есть ли там повторяющиеся строки.

При копировании данных из неотслеживаемого объекта в отслеживаемый объект необходимо убедиться, что копируются только значения, а не ссылки. Копируя неотслеживаемую ссылку, EF по умолчанию будет рассматривать ее как новую сущность. Даже если вы принудительно измените его состояние на Modified , DbContext уже может отслеживать объект с этим идентификатором.

Свойство If.Типы — это набор ссылок, таких как связь с поиском, и они могут меняться, когда добавляются и удаляются ассоциации, затем, чтобы применить изменения, вам нужно загрузить связанные типы из базы данных, а затем использовать это для удаления ассоциаций, которые больше не являются допустимыми, и добавить те, которые в настоящее время не связаны.

Например: учитывая свойство (PropertyA) с типами (Type1) и (Type2), если мы отредактируем его, чтобы иметь (Type1) и (Type3), нам нужно извлечь Type1 amp; Type3 из DbContext (отслеживаемого), а затем сравнить с отслеживаемым свойством, чтобы определить, нужно ли удалять Type2 и добавлять Type3

 var entity = await _context.Profiles
    .Include(x => x.Properties)
    .ThenInclude(x => x.Types)
    .SingleAsync(x => x.Id == profile.Id);

// Get the IDs for all Types we want to associate... In the above example this would
// ask for Type1 and Type3 if only the one property. We get a Distinct list because
// multiple properties might reference the same TypeId(s).
var existingTypeIds = profile.Properties
    .SelectMany(x => x.Types.Select(t => t.Id))
    .Distinct()
    .ToList();

// Load references to all Types that will be needed. Where associating new types, these will be referenced.
var existingTypes = _context.Types
    .Where(x => existingTypeIds.Contains(x.Id))
    .ToList();

foreach (var prop in profile.Properties)
{   
    existingProp = entity.Properties.SingleOrDefault(x => x.Id == prop.Id);
    if (existingProp == null)
        continue;

    var updatedTypeIds = prop.Types.Select(x => x.Id).ToList();
    var existingTypeIds = existingProp.Types.Select(x => x.Id).ToList();

    var addedTypeIds = updatedTypeIds.Except(existingTypeIds).ToList();
    var removedTypeIds = existingTypeIds.Except(updatedTypeIds).ToList();

    var addedTypes = existingTypes
        .Where(x => addedTypeIds.Contains(x.Id))
        .ToList();
    var removedTypes = existingProp.Types
        .Where(x => removedTypeIds.Contains(x.Id))
        .ToList();

    foreach(var removedType in removedTypes)
        existingProp.Types.Remove(removedType);

    foreach(var addedType in addedTypes)
        existingProp.Types.Add(addedType);

}
 

Если вместо этого тип является дочерней строкой, содержащей свойства, которые могут быть обновлены, тогда эти значения должны быть скопированы между обновленными данными и существующим состоянием данных. Это добавляет значительный объем работы, хотя такие инструменты, как AutoMapper, могут быть настроены, чтобы помочь. Вам все еще нужно управлять случаями, когда типы могут быть добавлены, удалены или содержимое изменено. Это относится и к свойствам, поскольку ваш пример обрабатывает только случаи, когда свойство обновляется, а не потенциально добавляется или удаляется.

В конечном счете может быть полезно попытаться структурировать сценарии обновления так, чтобы они были как можно более атомарными, чтобы избежать обновления, которое будет вносить изменения во весь объектный граф объектов, свойств и типов, а вместо этого одно обновление только для значений сущностей, одно для значений свойств и одно для обновления одного типа. Это также относится к добавлению свойства, добавлению типа, удалению свойства, удалению типа. Хотя может показаться, что для разделения подобных операций требуется больше кода, он делает их очень простыми и прямыми, а не одним большим и сложным методом, пытающимся сравнить «до» и «после», чтобы выяснить, что добавить, удалить и обновить. Ошибки скрываются в сложном коде, а не в простых методах. 🙂

При редактировании графа объектов вам также следует избегать вызова SaveChanges более одного раза. Вместо того, чтобы вызывать его в цикле свойств, это должно быть сделано один раз, когда цикл завершен. Причина этого в том, что что-то вроде исключения для одного из свойств приведет к сохранению неполного / недопустимого состояния данных. Если у вас есть 4 свойства в сохраняемом объекте, и 3-е по какой-либо причине завершается ошибкой с исключением, первые 2 будут обновлены, а последние два не сохраняются. Как правило, в рамках операции обновления обновление должно соответствовать принципу «все или ничего» для сохранения.

Надеюсь, это поможет объяснить поведение, которое вы видите, и даст вам что-то, что нужно рассмотреть для продвижения вперед.

Комментарии:

1. Спасибо за этот полный ответ, на самом деле я не мог ожидать, что будет так сложно! Я попросил обновить, поскольку у меня возникли проблемы с этим. Могу ли я обновить ваш ответ полным кодом (свойство (добавить / удалить / обновить), Типы (добавить / удалить / обновить))?