Ответ 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();
}
И он работает, как и ожидалось:
Через день г-н Смит называет банк: "Я передумал. Я не хочу, чтобы эти две учетные записи, только одна, одна с номером счета 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();
}
Он запускает тест, и он работает:
Прежде чем переводить код в производство, он решает добавить небольшое улучшение производительности, но, конечно же, не изменяет проверенную логику, чтобы переместить депозит и удалить учетную запись:
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 ничего нет:"
Что пошло не так, когда наш программист добавил небольшое улучшение производительности и сделал мистера Смита бедным человеком?
Важная строка, которая ведет себя по-другому после установки 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. Есть, вероятно, еще много.