我最近通过在执行批量删除之前禁用自动更改检测(Context.Configuration.AutoDetectChangesEnabled = false
),然后重新启用它并保存更改来调整部分运行速度非常慢的应用程序。
我读了几个不同的消息来源解释说,基本上,每当我在DbSet上调用.Add()
或.Remove()
之类的方法时,DetectChanges()
就会被调用,而且当这个问题变得昂贵时我们正在处理很多实体。行。
现在我想提请注意这些文章:
Entity Framework Automatic Detect Changes (MSDN)
禁用和重新启用的替代方法是始终关闭自动检测更改,并明确调用context.ChangeTracker.DetectChanges或努力使用更改跟踪代理。这两个选项都是高级的,可以轻松地将微小的错误引入您的应用程序,因此请小心使用它们。
Secrets of Detect Changes: Part 3
除非你真的需要,否则不要关闭自动DetectChanges;它只会让你痛苦。
也许它就在我的面前,但是假设,例如,我将.SaveChanges()
包装在一个总是先调用DetectChanges()
的方法中,我会遇到哪些错误,我通常不会遇到?我能看到的所有警告只是模糊地表明,如果不进入现状,就会发生不好的事情。
答案 0 :(得分:17)
假设我们有BankAccount
和Deposit
s的以下模型 - 一个简单的一对多关系: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();
}
它的工作方式与预期相符:
一天后,史密斯先生打电话给银行:&#34;我改变了主意。我不想要那两个帐户,只有一个,帐号为456的帐户,我更喜欢这个号码。在我的帐户123是100万美元。请将其移至帐户456,然后删除我的帐户123!&#34;
我们的程序员听说删除是一件危险的事情,并决定将数据库复制到测试环境中并首先测试他现在编写的新例程,以便遵循史密斯先生的要求:
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();
}
他运行测试并且有效:
在将代码转移到生产环境之前,他决定增加一点性能改进,但是 - 当然 - 并没有改变测试逻辑来移动存款并删除帐户:
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();
}
他在完成日常工作之前将代码投入生产。
第二天,史密斯先生打电话给银行:&#34;我的账户456需要50万!&#34;客服的店员说:&#34;对不起先生,但是你的账户没有钱456。&#34;史密斯先生:&#34;好吧,他们还没有动用这笔钱。然后,请从我的帐户123中取钱!&#34; &#34;对不起先生,但你没有帐户123!&#34;史密斯先生:&#34;什么???&#34;客户服务:&#34;我可以在我的银行工具中看到您的所有帐户和存款,而您的单个帐户456中没有任何内容:&#34;
当我们的程序员加入他的小小的性能提升并让史密斯先生成为一个可怜的人时出了什么问题?
将AutoDetectChangesEnabled
设置为false
后,行为不同的重要行是ctx.BankAccounts.Remove(bankAccount123);
。此行现在不再在内部调用DetectChanges
。结果是,EF无法了解BankAccountId
实体中的外键deposit
的更改(在调用Remove
之前发生)。
启用更改检测Remove
将根据更改的外键调整整个对象图(&#34;关系修正&#34;),即deposit.BankAccount
将设置为{{ 1}},bankAccount456
已从deposit
集合中删除,并已添加到bankAccount123.Deposits
集合中。
因为没有发生bankAccount456.Deposits
将父Remove
标记为bankAccount123
并将Deleted
- 仍然是deposit
中的bankAccount123.Deposits
收集 - 进入状态Deleted
。调用SaveChanges
时,两者都将从数据库中删除。
虽然这个例子看起来有点人为,但我记得我有类似的错误&#34;在实际代码中禁用更改检测后需要一些时间才能找到并理解。主要问题是,使用变更检测进行测试的代码可能不再起作用,并且在禁用更改检测后需要再次测试,即使该代码没有任何更改。也许必须修改代码以使其再次正常工作。 (在我们的示例中,程序员必须在 ctx.ChangeTracker.DetectChanges();
行之前添加Remove
来修复错误。)
这是可能的&#34; 微妙的错误之一&#34; MSDN页面正在谈论。可能还有更多。