通过禁用自动更改检测可以在EF中引起哪些错误?

我最近通过在执行批量删除之前禁用自动更改检测( Context.Configuration.AutoDetectChangesEnabled = false ),然后重新启用它并保存更改来调整部分运行速度非常慢的应用程序。

我读了几个不同的来源解释说,基本上,每当我在DbSet上调用像.Add()DetectChanges()这样的方法时,就会调用DetectChanges() ,当我们处理大量的实体。 好。

现在我想提请注意这些文章:

entity framework自动检测更改(MSDN)

禁用和重新启用的替代方法是始终关闭自动检测更改,并明确调用context.ChangeTracker.DetectChanges或努力使用更改跟踪代理。 这两个选项都是高级的,可以轻松地在您的应用程序中引入细微的错误,因此请小心使用它们。

检测变化的秘密:第3部分

除非你真的需要,否则不要关闭自动DetectChanges; 它会让你痛苦。

也许它就在我面前,但是假设,例如,我.SaveChanges()在一个总是调用DetectChanges()的方法中包装.SaveChanges() ,我会遇到哪些错误,我通常不会遇到? 我能看到的所有警告只是模糊地表明,如果不进入它们的本质,就会发生不好的事情。

假设我们有以下BankAccountDeposit的模型 – 一个简单的一对多关系: BankAccount有一个Deposit集合,一个Deposit属于一个BankAccount

 public class BankAccount { public int Id { get; set; } public int AccountNumber { get; set; } public string Owner { get; set; } public ICollection 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 BankAccounts { get; set; } public DbSet Deposits { get; set; } } 

约翰史密斯先生希望在我们的银行有两个账户,并向他的第一个账户支付1.000.000美元的押金。 我们银行的程序员就像这样完成这项任务:

 using (var ctx = new MyContext()) { var bankAccount123 = new BankAccount { AccountNumber = 123, Owner = "John Smith", Deposits = new List { 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的帐户,我更喜欢这个号码。我的帐户123是100万美元请将这些帐户移至帐户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's 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中获得50万!” 客服的店员说:“对不起先生,但你的账户上没有钱456.” 史密斯先生:“好吧,他们还没有动用这笔钱。然后,请从我的账户123拿走钱!” “对不起先生,但你没有账号123!” 史密斯先生:“什么???” 客户服务:“我可以在我的银行工具中看到您的所有帐户和存款,而您的单个帐户456上没有任何内容:”

DetectChanges 3


当我们的程序员增加了他的小小的性能提升并使史密斯先生成为一个穷人时出了什么问题?

AutoDetectChangesEnabled设置为false后,行为不同的重要行是ctx.BankAccounts.Remove(bankAccount123); 。 此行现在不再DetectChanges内部调用DetectChanges 。 结果是EF无法BankAccountId deposit实体中外键BankAccountId的变化(在调用Remove之前发生)。

启用更改检测Remove将根据更改的外键(“关系修正”)调整整个对象图,即deposit.BankAccount将设置为bankAccount456deposit将从bankAccount123.Deposits集合中删除并添加到bankAccount456.Deposits集合。

因为没有发生Remove标记了父bankAccount123Deleted并将deposit – 仍然是bankAccount123.Deposits集合中的子项 – 进入状态Deleted 。 调用SaveChanges ,两者都将从数据库中删除。

虽然这个例子看起来有点人为,但我记得在实际代码中禁用变更检测后我有类似的“错误”,需要一些时间才能找到并理解。 主要问题是工作和使用变更检测进行测试的代码可能不再起作用,并且需要在禁用更改检测后再次测试,即使该代码没有更改。 也许必须修改代码以使其再次正常工作。 (在我们的示例中,程序员必须在Remove之前添加ctx.ChangeTracker.DetectChanges();以修复错误。)

这是MSDN页面讨论的可能“ 微妙错误 ”之一。 可能还有更多。