Nieoczekiwany wyjątek InvalidOperationException podczas próby zmiany relacji za pomocą domyślnej wartości właściwości

10

W przykładowym kodzie poniżej otrzymuję następujący wyjątek db.Entry(a).Collection(x => x.S).IsModified = true:

System.InvalidOperationException: „Instancja typu jednostki„ B ”nie może być śledzona, ponieważ inna instancja o wartości klucza„ {Id: 0} ”jest już śledzona. Podczas dołączania istniejących encji upewnij się, że dołączona jest tylko jedna instancja encji o danej wartości klucza.

Dlaczego nie dodaje zamiast instancji B?

O dziwo dokumentacja IsModifiednie określa InvalidOperationExceptionjako możliwego wyjątku. Nieprawidłowa dokumentacja czy błąd?

Wiem, że ten kod jest dziwny, ale napisałem go tylko po to, aby zrozumieć, jak działa ef core w niektórych dziwnych przypadkach egde. Chcę wyjaśnienia, a nie obejścia.

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();
        }
    }
}
Supremum
źródło
W jaki sposób powiązane są A i B? co oznacza właściwość relacji?
Sam

Odpowiedzi:

8

Przyczyna błędu w podanym kodzie jest następująca.

Po utworzeniu encji Az bazy danych jej właściwość Sjest inicjowana kolekcją zawierającą dwa nowe rekordy B. Idkażdego z tych nowych Bbytów jest równy 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 {} };

Po wykonaniu wiersza kodu var a = db.Set<A>().Single()zbiór Sencji Anie zawiera Bencji z bazy danych, ponieważ DbContext Dbnie używa leniwego ładowania i nie ma jawnego ładowania kolekcji S. Jednostka Azawiera tylko nowe Bjednostki, które zostały utworzone podczas inicjowania kolekcji S.

Podczas wywoływania środowiska encji IsModifed = truekolekcji Spróbuje dodać te dwa nowe elementy Bdo śledzenia zmian. Ale zawodzi, ponieważ oba nowe Bpodmioty mają to samo 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;

Ze śladu stosu widać, że struktura encji próbuje dodać Bencje do 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)

Komunikat o błędzie mówi również, że nie może śledzić Bencji, Id = 0ponieważ inna Bencja z tym samym Idjest już śledzona.


Jak rozwiązać ten problem.

Aby rozwiązać ten problem, należy usunąć kod, który tworzy Bjednostki podczas inicjowania Skolekcji:

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

Zamiast tego należy wypełnić Skolekcję w miejscu, w którym Azostał utworzony. Na przykład:

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

Jeśli nie używasz leniwego ładowania, należy jawnie załadować Skolekcję, aby dodać jej elementy do śledzenia zmian:

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

Dlaczego nie dodaje zamiast instancji B?

Krótko mówiąc , są dołączeni natychmiast po dodaniu, ponieważ mają Detachedstan.

Po wykonaniu wiersza kodu

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

utworzone wystąpienia encji Bmają stan Detached. Można to zweryfikować za pomocą następnego kodu:

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

Potem, kiedy ustawisz

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

EF próbuje dodać Bpodmioty, aby zmienić śledzenie. Z kodu źródłowego EFCore widać, że prowadzi nas to do metody InternalEntityEntry.SetPropertyModified z następnymi wartościami argumentów:

  • property- jeden z naszych Bpodmiotów,
  • changeState = true,
  • isModified = true,
  • isConceptualNull = false,
  • acceptChanges = true.

Ta metoda z takimi argumentami zmienia stan Detached Bentites Modified, a następnie próbuje rozpocząć ich śledzenie (patrz wiersze 490 - 506). Ponieważ Bjednostki mają teraz stan, Modifiedprowadzi to do ich przyłączenia (nie dodania).

Iliar Turdushev
źródło
Gdzie jest odpowiedź na pytanie „Dlaczego nie dodaje się zamiast dołączać wystąpienia B?” Mówisz „to się nie udaje, ponieważ obie nowe jednostki B mają ten sam Id = 0”. Myślę, że to jest złe, ponieważ ef core zapisuje zarówno identyfikatory 1, jak i 2. Nie sądzę, aby na to pytanie
udzielono
@DIlshod K Dzięki za komentarz. W sekcji „Jak rozwiązać ten problem” napisałem, że kolekcja Spowinna być ładowana jawnie, ponieważ podany kod nie używa leniwego ładowania. Oczywiście EF zapisał wcześniej utworzone Bjednostki w bazie danych. Ale wiersz kodu A a = db.Set<A>().Single()ładuje tylko encję Abez encji w kolekcji S. Aby załadować kolekcję, Snależy użyć szybkiego ładowania. Zmienię moją odpowiedź, aby wyraźnie zawierała odpowiedź na pytanie „Dlaczego nie dodaje się zamiast dołączać wystąpienia B?”.
Iliar Turdushev