Каковы ошибки, которые могут быть вызваны в EF, отключив автоматическое обнаружение изменений?

Недавно я модифицировал часть моего приложения, которое выполнялось очень медленно, отключив автоматическое обнаружение изменений (Context.Configuration.AutoDetectChangesEnabled = false) перед выполнением массового удаления, затем повторно включив его и сохранив изменения.

Я прочитал несколько разных источников, объясняющих это, по сути, всякий раз, когда я вызываю методы типа .Add() или .Remove() на DbSet, вызывается DetectChanges(), и это может стать дорогостоящим, когда мы имеем дело с партиями сущностей. OK.

Теперь я хочу обратить внимание на эти статьи, в частности:

Автоматическое обнаружение изменений Entity Framework (MSDN)

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

Секреты обнаружения изменений: Часть 3

Не отключайте автоматический DetectChanges, если вам действительно не нужно; это просто причинит вам боль.

Возможно, это передо мной, но, предположив, что, например, я обернул .SaveChanges() в методе, который всегда назывался DetectChanges(), во-первых, какие ошибки я мог бы начать сталкиваться с тем, что я обычно не буду? Все предупреждения, которые я вижу, просто смутно говорят о том, что плохие вещи могут произойти, не вникая в то, что они есть.

Ответ 1

Предположим, что у нас есть следующая модель BankAccount и Deposit - простое отношение "один ко многим": BankAccount имеет набор Deposit, а Deposit принадлежит к одному BankAccount:

public class BankAccount
{
    public int Id { get; set; }
    public int AccountNumber { get; set; }
    public string Owner { get; set; }
    public ICollection<Deposit> Deposits { get; set; }
}

public class Deposit
{
    public int Id { get; set; }
    public decimal Value { get; set; }

    public int BankAccountId { get; set; }
    public BankAccount BankAccount { get; set; }
}

И простой контекст базы данных:

public class MyContext : DbContext
{
    public DbSet<BankAccount> BankAccounts { get; set; }
    public DbSet<Deposit> Deposits { get; set; }
}

г. Джон Смит хочет иметь два счета в нашем банке и выплачивает депозит в размере 1.000.000 $на свой первый счет. Наш банковский программист выполняет эту задачу следующим образом:

using (var ctx = new MyContext())
{
    var bankAccount123 = new BankAccount
    {
        AccountNumber = 123,
        Owner = "John Smith",
        Deposits = new List<Deposit> { new Deposit { Value = 1000000m } }
    };
    var bankAccount456 = new BankAccount
    {
        AccountNumber = 456,
        Owner = "John Smith"
    };

    ctx.BankAccounts.Add(bankAccount123);
    ctx.BankAccounts.Add(bankAccount456);

    ctx.SaveChanges();
}

И он работает, как и ожидалось:

DetectChanges 1

Через день г-н Смит называет банк: "Я передумал. Я не хочу, чтобы эти две учетные записи, только одна, одна с номером счета 456, мне нравится этот номер лучше. 1 миллион долларов. Пожалуйста, переместите их на счет 456, а затем удалите мою учетную запись 123!"

Наш программист слышал, что удаление опасно и решило скопировать базу данных в тестовую среду и сначала протестировать новую рутину, которую он пишет, чтобы следовать запросу г-на Смита:

using (var ctx = new MyContext())
{
    var bankAccount123 = ctx.BankAccounts.Include(b => b.Deposits)
        .Single(b => b.AccountNumber == 123);
    var bankAccount456 = ctx.BankAccounts
        .Single(b => b.AccountNumber == 456);
    var deposit = bankAccount123.Deposits.Single();

    // here our programmer moves the deposit to account 456 by changing
    // the deposit account foreign key
    deposit.BankAccountId = bankAccount456.Id;

    // account 123 is now empty and can be deleted safely, he thinks!
    ctx.BankAccounts.Remove(bankAccount123);

    ctx.SaveChanges();
}

Он запускает тест, и он работает:

DetectChanges 2

Прежде чем переводить код в производство, он решает добавить небольшое улучшение производительности, но, конечно же, не изменяет проверенную логику, чтобы переместить депозит и удалить учетную запись:

using (var ctx = new MyContext())
{
    // he added this well-known line to get better performance!
    ctx.Configuration.AutoDetectChangesEnabled = false;

    var bankAccount123 = ctx.BankAccounts.Include(b => b.Deposits)
        .Single(b => b.AccountNumber == 123);
    var bankAccount456 = ctx.BankAccounts
        .Single(b => b.AccountNumber == 456);
    var deposit = bankAccount123.Deposits.Single();

    deposit.BankAccountId = bankAccount456.Id;

    ctx.BankAccounts.Remove(bankAccount123);

    // he heard this line would be required when AutoDetectChanges is disabled!
    ctx.ChangeTracker.DetectChanges();
    ctx.SaveChanges();
}

Он запускает код в процессе производства, прежде чем заканчивает свою ежедневную работу.

На следующий день г-н Смит называет банк: "Мне нужно полмиллиона от моего счета 456!" Клерк при обслуживании клиентов говорит: "Извините, сэр, но на вашем счете нет денег 456." Мистер Смит: "Хорошо, они еще не перевели деньги. Тогда, пожалуйста, возьмите деньги со своего счета 123!" "Извините, сэр, но у вас нет учетной записи 123!" Г-н Смит: "ЧТО???" Обслуживание клиентов: "Я вижу все ваши счета и депозиты в своем банковском инструменте, и в вашей отдельной учетной записи 456 ничего нет:"

DetectChanges 3


Что пошло не так, когда наш программист добавил небольшое улучшение производительности и сделал мистера Смита бедным человеком?

Важная строка, которая ведет себя по-другому после установки AutoDetectChangesEnabled в false, равна ctx.BankAccounts.Remove(bankAccount123);. Эта строка теперь больше не вызывает DetectChanges внутренне. В результате EF не получает знания об изменении внешнего ключа BankAccountId в объекте Deposit (который произошел до вызова Remove).

С включенным обнаружением изменений Remove скорректировал весь график объекта в соответствии с измененным внешним ключом ( "fix fix" ), то есть deposit.BankAccount был бы установлен в bankAccount456, Deposit был бы удаляется из коллекции bankAccount123.Deposits и добавляется в коллекцию bankAccount456.Deposits.

Так как этого не произошло, Remove помещал родительский bankAccount123 как Deleted и помещал Deposit - который все еще является дочерним элементом в коллекции bankAccount123.Deposits - в состояние Deleted. Когда вызывается SaveChanges, оба удаляются из базы данных.

Хотя этот пример выглядит немного искусственным, я помню, что у меня были подобные "ошибки" после отключения обнаружения изменений в реальном коде, что заняло некоторое время, чтобы найти и понять. Основная проблема заключается в том, что код, который работает и протестирован с обнаружением изменений, возможно, больше не работает и нуждается в повторном тестировании после того, как обнаружение изменений отключено, хотя ничего не изменилось с этим кодом. И, возможно, код должен быть изменен, чтобы он снова работал правильно. (В нашем примере программисту пришлось добавить ctx.ChangeTracker.DetectChanges(); до строки Remove, чтобы исправить ошибку.)

Это один из возможных "тонких ошибок", о которых говорит страница MSDN. Есть, вероятно, еще много.