2015年4月4日

Entity Framework のパフォーマンス #2 更新処理

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>.AddDbSet<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>.AddRangeDbSet<TEntity>.RemoveRange が導入されているので、常に「AutoDetectChangesEnabled = true」のままで十分かも。

0 件のコメント:

コメントを投稿