2015年4月14日

Entity Framework でアトミックインクリメント & 一括更新

Entity Framework (EF) 標準の更新処理では、アトミックインクリメント(デクリメントも)や一括更新(BULK UPDATE/DELETE)ができない。
一括処理については、条件に当てはまるものを select 後に update ないし delete は可能だが、件数が多い場合はかなり効率の悪い処理になる。(select → update/delete × 件数)
そこだけ文字列 SQL 使えば解決・・・なんだけど、せっかく EF 使ってるので文字列 SQL は避けたい、という人は多いと思う。

そういう要望に応えるライブラリがいくつかある。(非 MS 製) ※すべて NuGet からインストール可能

ここでは、一番よく使われているであろう EntityFramework.Extended を触ってみる。
※ EntityFramework.Utilities は EF 6.0 以降がメインだし、ZZZ は有料なので

[環境]
  • Visual Studio 2010 SP1
  • .NET 4.5.2 (TargetFramework は 4.0)
  • Entity Framework 5.0
  • EntityFramework.Extended 5.0.0.73

EntityFramework.Extended サンプルコード

[コード]

// テーブル定義
public partial class CountTable
{
    public string Name { get; set; }
    public int Value { get; set; }
    public System.DateTime Updated { get; set; }
}

static class EExfTest
{
    // アトミックインクリメント
    public static void UpdateIncrement()
    {
        using( var context = new TestEntities() ){
            context.CountTables.Update(
                item => item.Name == "aaa",
                item => new CountTable {
                    Value = item.Value + 1,
                    Updated = EntityFunctions.AddDays(item.Updated, 1).Value
                }
            );
        }
        // Update は IQueryable に対する拡張メソッド
        // 第 1 引数は where 条件、第 2 引数は set の中身 (オブジェクト初期化子で指定する)
        // 両引数は式木として解釈される
        // where 条件を指定しないオーバーロードもあり
        //
        // 数値のインクリメントだけでなく、EntityFunctions を使って日付のインクリメントも可能
    }

    // 一括変更
    public static void BulkUpdate()
    {
        using( var context = new TestEntities() ){
            context.CountTables.Update(
                item => new CountTable {
                    Value = 0
                }
            );
        }
    }

    // 一括削除
    public static void BulkDelete()
    {
        using( var context = new TestEntities() ){
            context.CountTables
                .Where(item => item.Value < 1)
                .Delete();
        }
        // Delete は IQueryable に対する拡張メソッド
        // 第 1 引数(where 条件)を指定するオーバーロードもあり
    }
}
[実行される SQL]

-- UpdateIncrement
UPDATE [dbo].[CountTable] SET 
[Value] = [Value] + 1 , 
[Updated] = DATEADD (day, 1, [Updated])  
FROM [dbo].[CountTable] AS j0 INNER JOIN (
SELECT 
1 AS [C1], 
[Extent1].[Name] AS [Name]
FROM [dbo].[CountTable] AS [Extent1]
WHERE 'aaa' = [Extent1].[Name]
) AS j1 ON (j0.[Name] = j1.[Name])

-- BulkUpdate
-- ※実際は sp_executesql で実行
UPDATE [dbo].[CountTable] SET 
[Value] = 0
FROM [dbo].[CountTable] AS j0 INNER JOIN (
SELECT 
1 AS [C1], 
[Extent1].[Name] AS [Name]
FROM [dbo].[CountTable] AS [Extent1]
) AS j1 ON (j0.[Name] = j1.[Name]

-- BulkDelete
DELETE [dbo].[CountTable]
FROM [dbo].[CountTable] AS j0 INNER JOIN (
SELECT 
[Extent1].[Value] AS [Value], 
[Extent1].[Name] AS [Name]
FROM [dbo].[CountTable] AS [Extent1]
WHERE [Extent1].[Value] < 1
) AS j1 ON (j0.[Name] = j1.[Name])

EntityFramework.Extended のトランザクション

EF.Extended の Update と Delete は、ExecuteSqlCommand と同じく即時 SQL が実行される。
つまり、DbContext.SaveChanges と無関係のトランザクションになる。

EF 標準の更新処理と EF.Extended による更新処理を混ぜる場合は、自分でトランザクションを管理する必要がある。

[トランザクション使用例]

using( var context = new TestEntities() )
using( var tx = context.BeginTransaction() ){
    // 更新処理

    context.SaveChanges();
    tx.Commit();
}

// BeginTransaction も EF.Extended にある拡張メソッド
// EF 6.0 からは context.Database.BeginTransaction() が可能になったので、Obsolete に

EntityFramework.Extended の対応 DB

現在(2015/04/13)の EF.Extended のソースを見る限り、SQL Server にしか対応してない。
※ /Source/EntityFramework.Extended/Batch 配下に SqlServerBatchRunner.cs しかないため

ただし、EF の接続オブジェクト ObjectContext.Connection を利用するため、他の DB も一応は使用可能。
(その DB が扱えない SQL が構築されると、DB 側でエラーになる。)

0 件のコメント:

コメントを投稿