Неожиданное исключение InvalidOperationException при попытке изменить отношение через значение свойства по умолчанию

10

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

System.InvalidOperationException: «Экземпляр типа сущности« B »не может быть отслежен, поскольку другой экземпляр со значением ключа« {Id: 0} »уже отслеживается. При подключении существующих объектов убедитесь, что подключен только один экземпляр объекта с данным значением ключа.

Почему он не добавляет, а не прикрепляет экземпляры B?

Странно, что документация IsModifiedне указывает InvalidOperationExceptionкак возможное исключение. Неверная документация или ошибка?

Я знаю, что этот код странный, но я написал его только для того, чтобы понять, как работает ядро ​​в некоторых странных случаях 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();
        }
    }
}
Supremum
источник
Как связаны А и В? то есть что такое свойство отношений?
Сэм

Ответы:

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 Bобъектов 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?».
Ильяр Турдушев