Entity Framework (EF) の更新処理 (insert, update, delete) は、
AutoDetectChangesEnabled に false を設定するだけでパフォーマンスを向上できることがある。
[AutoDetectChangesEnabled 設定方法]
using( var context = new SampleEntities() ){
context.Configuration.AutoDetectChangesEnabled = false;
// 更新処理
}
パフォーマンスが向上するケース
[環境]
- Windows 7 64bit
- Visual Studio 2010 SP1
- .NET 4.5.2 (TargetFramework は 4.0)
- Entity Framework 5.0 + Database First + DbContext
[測定コード]
※テーブル定義、経過時間測定メソッドは
この記事と同じ
// 測定処理
static class EfTest
{
// KeyTable1 の追加処理(insert)の時間
public static void Key1Insert(bool autoDetect, int count)
{
using( var context = new TestEntities() ){
context.Configuration.AutoDetectChangesEnabled = autoDetect;
// 既存データを削除
context.Database.ExecuteSqlCommand("delete from KeyTable1");
// 追加データを作成
var date = DateTime.Now;
var data =
Enumerable.Range(0, count)
.Select(i => new KeyTable1 {
Key = i.ToString(),
Name = "No." + i,
Register = date
}).ToArray();
Measurement.ConsoleElapsedTime(() => {
foreach( var d in data ) context.KeyTable1.Add(d);
context.SaveChanges();
});
}
}
// KeyTable1 の削除処理(delete)の時間
public static void Key1Delete(bool autoDetect, int count)
{
using( var context = new TestEntities() ){
context.Configuration.AutoDetectChangesEnabled = autoDetect;
// 既存データを削除
context.Database.ExecuteSqlCommand("delete from KeyTable1");
// 削除データを追加
for( var i = 0; i < count; i++ ){
context.Database.ExecuteSqlCommand(
"insert into KeyTable1 values({0}, '_', getdate())", i);
}
// 削除データを取得
var data = context.KeyTable1.ToArray();
Measurement.ConsoleElapsedTime(() => {
foreach( var d in data ) context.KeyTable1.Remove(d);
context.SaveChanges();
});
}
}
}
ケース1. 複数件追加
bool autoDetect; // true or false
DbContextLayer.Key1Insert(autoDetect, 1); // ウォームアップ用
DbContextLayer.Key1Insert(autoDetect, 100);
DbContextLayer.Key1Insert(autoDetect, 1000);
AutoDetectChangesEnabled = true
[Key1Insert] 00:00:00.1478355
[Key1Insert] 00:00:00.1596723
[Key1Insert] 00:00:01.9620471
AutoDetectChangesEnabled = false
[Key1Insert] 00:00:00.1454600
[Key1Insert] 00:00:00.1597164
[Key1Insert] 00:00:01.5293208
1 回目はウォームアップ用なので無視。
100 件では同じくらいだが、1000 件になると差が出る。
また、「AutoDetectChangesEnabled = true」の場合、件数の増加と実行時間が非線形。
ケース2. 複数件削除
bool autoDetect; // true or false
DbContextLayer.Key1Delete(autoDetect, 1); // ウォームアップ用
DbContextLayer.Key1Delete(autoDetect, 100);
DbContextLayer.Key1Delete(autoDetect, 1000);
AutoDetectChangesEnabled = true
[Key1Delete] 00:00:00.0689035
[Key1Delete] 00:00:00.1432008
[Key1Delete] 00:00:02.3928906
AutoDetectChangesEnabled = false
[Key1Delete] 00:00:00.0662479
[Key1Delete] 00:00:00.1323060
[Key1Delete] 00:00:01.3451472
ケース1. と似たような結果。
違いは、「AutoDetectChangesEnabled = true」の場合の、ケース2. の方が、件数増加による実行時間の増加が大きいこと。
パフォーマンスが向上する理由
くわしくは上記参照だけど、長いのでまとめる。
DbSet<TEntity>.Add や
DbSet<TEntity>.Remove は、DetectChanges() というコストの高いメソッドが呼ばれているため。
「AutoDetectChangesEnabled = false」とすると、これを呼ばないようにできる。
ケース1. とケース2. でパフォーマンスが向上しているのは、DetectChanges() の呼び出し回数が減っているため。
DetectChanges() の処理を簡単に言うと、DB から取得したデータと、現在のデータを比べて、変更があればデータのステータス
EntityState を更新している。
しかし、DbSet<TEntity>.Add や DbSet<TEntity>.Remove は、対象データのステータスを EntityState.Added や EntityState.Deleted に更新するので、DetectChanges() の実行は不要。
(不要なのに呼ばれる実装になっている理由は不明・・・)
AutoDetectChangesEnabled = false の注意点
上記の通り、追加処理 (insert) と削除処理 (delete) は問題ないのだが、変更処理 (update) の場合、通常処理のままだと問題が出てくる。
[更新できない変更処理]
using( var context = new SampleEntities() ){
context.Configuration.AutoDetectChangesEnabled = false;
var data = context.KeyTable1.First(); // KeyTable1 は 1 件以上
data.Name = "First";
context.SaveChanges();
}
このコードでは DB に対する更新処理は実行されない。
理由は、context.SaveChanges() で DetectChanges() が呼ばれないようになっているため、EF がデータの更新を検知できず、update 文が実行されないため。
[更新できない変更処理の修正例]
using( var context = new SampleEntities() ){
context.Configuration.AutoDetectChangesEnabled = false;
var data = context.KeyTable1.First(); // KeyTable1 は 1 件以上
data.Name = "First";
// DetectChanges の手動呼び出し
context.ChangeTracker.DetectChanges();
// または、ステータスの手動変更
context.Entry(data).State = EntityState.Modified;
// いずれかだけでよい
context.SaveChanges();
}
ただ、SaveChanges を 1 回しか使わない場合(=DetectChanges の呼び出し回数が変わらない)は「AutoDetectChangesEnabled = false」としても効果はほぼないので、デフォルト(true)のままがベストだと思われる。
まとめ
結局、AutoDetectChangesEnabled の設定をどうすべきかは、ケースバイケースになる。
基本的な方針としては、
1 つのトランザクションで複数件の追加・削除がある場合のみ、false を設定。
そのトランザクションに変更が含まれる場合、 SaveChanges 前に DetectChanges を呼び出すか、 EntityState を変更。
常に「AutoDetectChangesEnabled = false」でもいいかもしれないが、上のURL にある注意点に、複雑なことをやると SaveChanges 前に DetectChanges を呼び出すだけでは対応できないケースがあるとか・・・
EF の更新処理は「AutoDetectChangesEnabled = true」がデフォルトとして実装されているので、基本的に変更しない方がいいらしい。
また、EF6.0 から複数件の追加・削除を考慮した
DbSet<TEntity>.AddRange と
DbSet<TEntity>.RemoveRange が導入されているので、常に「AutoDetectChangesEnabled = true」のままで十分かも。