Неочікувана InvalidOperationException при спробі змінити співвідношення через значення властивості за замовчуванням


10

У наведеному нижче прикладі коду я отримую таке виключення під час виконання db.Entry(a).Collection(x => x.S).IsModified = true:

System.InvalidOperationException: 'Екземпляр типу сутності' B 'неможливо відстежити, оскільки інший екземпляр зі значенням ключа' {Id: 0} 'вже відстежується. Укладаючи існуючі об'єкти, переконайтеся, що додано лише один екземпляр сутності з заданим значенням ключа.

Чому він не додає замість приєднання екземплярів B?

Як не дивно, документація IsModifiedне вказує InvalidOperationExceptionяк можливий виняток. Недійсна документація чи помилка?

Я знаю, що цей код дивний, але я написав його лише для того, щоб зрозуміти, як ядро ​​ef працює в деяких дивних випадках egde. Те, що я хочу, - це пояснення, а не обробка.

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    public class A
    {
        public int Id { get; set; }
        public ICollection<B> S { get; set; } = new List<B>() { new B {}, new B {} };
    }

    public class B
    {
        public int Id { get; set; }
    }

    public class Db : DbContext {
        private const string connectionString = @"Server=(localdb)\mssqllocaldb;Database=Apa;Trusted_Connection=True";

        protected override void OnConfiguring(DbContextOptionsBuilder o)
        {
            o.UseSqlServer(connectionString);
            o.EnableSensitiveDataLogging();
        }

        protected override void OnModelCreating(ModelBuilder m)
        {
            m.Entity<A>();
            m.Entity<B>();
        }
    }

    static void Main(string[] args)
    {
        using (var db = new Db()) {
            db.Database.EnsureDeleted();
            db.Database.EnsureCreated();

            db.Add(new A { });
            db.SaveChanges();
        }

        using (var db = new Db()) {
            var a = db.Set<A>().Single();
            db.Entry(a).Collection(x => x.S).IsModified = true;
            db.SaveChanges();
        }
    }
}

Як пов’язані між собою А і В? що означає властивість відносин?
Сам

Відповіді:


8

Причина помилки у наданому коді наступна.

Коли ви створюєте об'єкт Aіз бази даних, його властивість Sініціалізується колекцією, що містить дві нові записи B. Idкожного з цих нових Bутворень дорівнює 0.

// This line of code reads entity from the database
// and creates new instance of object A from it.
var a = db.Set<A>().Single();

// When new entity A is created its field S initialized
// by a collection that contains two new instances of entity B.
// Property Id of each of these two B entities is equal to 0.
public ICollection<B> S { get; set; } = new List<B>() { new B {}, new B {} };

Після виконання рядка var a = db.Set<A>().Single()збору коду Sсутність Aне містить Bоб'єктів із бази даних, тому DbContext Dbщо не використовує ледачу завантаження і немає явного завантаження колекції S. Суб'єкт Aмістить лише нові Bоб'єкти, які були створені під час ініціалізації колекції S.

Коли ви звертаєтесь IsModifed = trueдо структури сукупності колекції, він Sнамагається додати ці два нові елементи Bу відстеження змін. Але це не вдається, тому що обидва нові Bсуб'єкти мають однакове Id = 0:

// This line tries to add to change tracking two new B entities with the same Id = 0.
// As a result it fails.
db.Entry(a).Collection(x => x.S).IsModified = true;

З трасування стека видно, що структура сутності намагається додати Bоб'єкти до IdentityMap:

at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.ThrowIdentityConflict(InternalEntityEntry entry)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.Add(TKey key, InternalEntityEntry entry, Boolean updateDuplicate)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.Add(TKey key, InternalEntityEntry entry)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.Add(InternalEntityEntry entry)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.StartTracking(InternalEntityEntry entry)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetPropertyModified(IProperty property, Boolean changeState, Boolean isModified, Boolean isConceptualNull, Boolean acceptChanges)
at Microsoft.EntityFrameworkCore.ChangeTracking.NavigationEntry.SetFkPropertiesModified(InternalEntityEntry internalEntityEntry, Boolean modified)
at Microsoft.EntityFrameworkCore.ChangeTracking.NavigationEntry.SetFkPropertiesModified(Object relatedEntity, Boolean modified)
at Microsoft.EntityFrameworkCore.ChangeTracking.NavigationEntry.set_IsModified(Boolean value)

І повідомлення про помилку також говорить про те, що воно не може відстежувати Bсутність, Id = 0оскільки інша Bсутність з такою ж Idуже відслідковується.


Як вирішити цю проблему.

Щоб вирішити цю проблему, слід видалити код, який створює Bсутності при ініціалізації Sколекції:

public ICollection<B> S { get; set; } = new List<B>();

Замість цього слід заповнити Sколекцію у тому місці, де Aстворено. Наприклад:

db.Add(new A {S = {new B(), new B()}});

Якщо ви не використовуєте ледаче завантаження, вам слід явно завантажити Sколекцію, щоб додати її елементи до відстеження змін:

// Use eager loading, for example.
A a = db.Set<A>().Include(x => x.S).Single();
db.Entry(a).Collection(x => x.S).IsModified = true;

Чому він не додає замість приєднання екземплярів B?

Коротше кажучи , вони додаються, щоб їх додавали, оскільки вони мають Detachedдержавну.

Після виконання рядка коду

var a = db.Set<A>().Single();

створені екземпляри суб'єкта Bмають стан Detached. Це можна підтвердити за допомогою наступного коду:

Console.WriteLine(db.Entry(a.S[0]).State);
Console.WriteLine(db.Entry(a.S[1]).State);

Потім, коли ви встановите

db.Entry(a).Collection(x => x.S).IsModified = true;

EF намагається додати Bоб'єкти для зміни відстеження. З вихідного коду EFCore видно, що це призводить нас до методу InternalEntityEntry.SetPropertyModified із наступними значеннями аргументу:

  • property- одна з наших Bорганізацій,
  • changeState = true,
  • isModified = true,
  • isConceptualNull = false,
  • acceptChanges = true.

Цей метод з такими аргументами змінює стан Detached Bentites до Modified, а потім намагається почати їх відстеження (див. Рядки 490 - 506). Оскільки Bсуб'єкти мають стан, Modifiedце призводить до того, що вони додаються (не додаються).


Де відповідь "Чому він не додає замість приєднання екземплярів B?" Ви говорите: "це не вдається, оскільки обидва нові B об'єкти мають однаковий Id = 0". Я думаю, що це неправильно, оскільки ef core зберігає і 1, і 2 ідентифікатори. Я не думаю, що це правильна відповідь на це запитання
DIlshod K

@DIlshod K Дякую за коментар. У розділі "Як вирішити цю проблему" я написав, що колекцію Sслід завантажувати явно, оскільки наданий код не використовує ледачого завантаження. Звичайно, EF зберігає раніше створені Bоб'єкти в базі даних. Але рядок коду A a = db.Set<A>().Single()завантажує лише сутність Aбез сутності в колекції S. Для завантаження колекції Sпотрібно використовувати завантаження. Я зміню свою відповідь, щоб явно включити відповідь на запитання "Чому вона не додається замість приєднання екземплярів B?".
Іліар Турдушев
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.