ファントム リードとその解決方法 その 1

(目次: トランザクションのサンプル)

ファントム リード (Phantom Read) とは、
同一のトランザクション内でテーブルを複数回読み取ったときに、途中でレコードが増えてしまう現象のことです。
今回は、SQL Server におけるファントム リードの発生と解決方法について、実際のコードを示して説明します。

前提として、SQL Server におけるロック メカニズムやトランザクション分離レベルについての基礎知識が必要になります。
これらは SQL Server のロック管理 (@IT) で確認できます。

以下で示すサンプルでは同時実行制御のために自動トランザクション (TransactionScope クラス) を使用していますが、
手動トランザクションなどを使用しても同様です。
さらに、データ アクセスのために SqlCommand クラスを使用していますが、
型指定された TableAdapter などを使用しても同様です。

■ ステップ 1: Repeatable Read (→ファントム リード発生)

先にコードを示します。
コンソール アプリケーション プロジェクトを作成し、Program.cs に次のように記述します。
System.Transactions.dll への参照も必要です。


using System;
using System.Data.SqlClient;
using System.Threading.Tasks;
using System.Transactions;

namespace PhantomRead
{
    class Program
    {
        private const string NorthwindConnectionString = @"Data Source=.\SQLExpress;Initial Catalog=Northwind;Integrated Security=True";

        private const string SelectCommandText = "select count(1) from Categories";
        private const string InsertCommandText = "insert into Categories values (N’New Category’, null, null)";

        private static readonly TransactionOptions transactionOptions = new TransactionOptions { IsolationLevel = IsolationLevel.RepeatableRead };

        private static int phantomCount;

        static void Main(string[] args)
        {
            // 挿入と読み取りを並列に実行します。
            Parallel.Invoke(
                () =>
                {
                    for (int i = 0; i < 200; i++)
                    {
                        InsertCategory();
                    }
                },
                () =>
                {
                    for (int i = 0; i < 100; i++)
                    {
                        SelectCategoriesCount();
                    }
                }
            );
        }

        /// <summary>
        /// Northwind データベースの Categories テーブルの行を挿入します。
        /// </summary>
        private static void InsertCategory()
        {
            using (var scope = new TransactionScope(TransactionScopeOption.Required, transactionOptions))
            using (var connection = new SqlConnection(NorthwindConnectionString))
            {
                // データベース接続を開きます。
                connection.Open();

                // カテゴリを追加します。
                using (var command = new SqlCommand(InsertCommandText, connection))
                {
                    command.ExecuteNonQuery();
                }

                // コミットします。
                scope.Complete();
            }
        }

        /// <summary>
        /// Northwind データベースの Categories テーブルの行数を読み取ります。
        /// </summary>
        private static void SelectCategoriesCount()
        {
            using (var scope = new TransactionScope(TransactionScopeOption.Required, transactionOptions))
            using (var connection = new SqlConnection(NorthwindConnectionString))
            {
                // データベース接続を開きます。
                connection.Open();

                // カテゴリの数を読み取ります。
                using (var command = new SqlCommand(SelectCommandText, connection))
                {
                    int count1 = (int)command.ExecuteScalar();
                    int count2 = (int)command.ExecuteScalar();

                    if (count1 != count2)
                    {
                        Console.WriteLine("{0} = {1} ({2} 回目)", count1, count2, ++phantomCount);
                    }
                    else
                    {
                        Console.WriteLine("{0} = {1}", count1, count2);
                    }
                }
            }
        }
    }
}


今回は Northwind データベースの Categories テーブルを使用します。
最初は 8 レコードだけ登録されています。
また、CategoryID 列には IDENTITY が指定されています。

Categories テーブルのデータ

InsertCategory メソッドは、Categories テーブルのレコードを 1 件だけ追加します。

SelectCategoriesCount メソッドは、Categories テーブルの全レコード数を 2 回読み取って比較します。
トランザクション分離レベルを Repeatable Read に設定して、一連の処理を TransactionScope で囲みます。

そしてこの 2 つのメソッドを並列に実行します。
なお、並列処理には .NET Framework 4 で追加されたタスク並列ライブラリ (TPL) を使用しています。

ステップ 1 実行結果

実行結果は上の図のようになります。
SelectCategoriesCount メソッドを 100 回実行して 20 回ほどは同一トランザクション内で値が変化してしまっています。
これがファントム リードという現象です。
同一トランザクション内では、何度読み取っても同じ値にならなければ一貫性を満たしているとは言えません。

Repeatable Read の場合には、共有ロックの対象が行自身のみであるために、別のトランザクションから行の挿入ができてしまいます。

■ ステップ 2: Serializable (→解決)

トランザクション分離レベルを Serializable に変更します。


private static readonly TransactionOptions transactionOptions = new TransactionOptions { IsolationLevel = IsolationLevel.Serializable };


コードを上記のように 1 行だけ書き換えて実行します。

ステップ 2 実行結果

すると、今度は期待通りに動作します。
Serializable であれば範囲共有ロックを取得するため、別のトランザクションは行を挿入できなくなります。

このように、Serializable は行の挿入が関連する業務で重大な役割を持っており、
例えば採番や行数の制限が必要な場合などで利用されます。

バージョン情報
.NET Framework 4
SQL Server 2008, 2008 R2

参照
.NET エンタープライズ Web アプリケーション開発技術大全 〈Vol.5〉 トランザクション設計編
エンタープライズ技術大全(トランザクション設計編) – WikiWiki (上記書籍の要約)
SQL Server のロック管理 (@IT)

広告

ノンリピータブル リードとその解決方法 その 2

前回のノンリピータブル リードとその解決方法 その 1 では SqlCommand クラスを利用したコードを示しましたが、
今回は ADO.NET Entity Framework を利用したコードを示します。
ただし、特に説明する部分はないので、ステップ 2 (Repeatable Read) の場合のみ示します。

なお、下記のコードを記述する前に、ロスト アップデートとその解決方法 その 3 と同様に
Visual Studio で ADO.NET Entity Data Model を作成しておきます。


using System;
using System.Data.Objects;
using System.Linq;
using System.Threading.Tasks;
using System.Transactions;

namespace NonrepeatableRead

    class Program
    {
        private static readonly TransactionOptions transactionOptions = new TransactionOptions { IsolationLevel = IsolationLevel.RepeatableRead }; 

        private static int isNonrepeatableCount;

        static void Main(string[] args)
        {
            // 更新と読み取りを並列に実行します。
            Parallel.Invoke(
                () =>
                {
                    for (int i = 0; i < 200; i++)
                    {
                        UpdateUnitsInStock();
                    }
                },
                () =>
                {
                    for (int i = 0; i < 100; i++)
                    {
                        SelectUnitsInStock();
                    }
                }
            );
        }

        /// <summary>
        /// Northwind データベースの Products テーブルの UnitsInStock の値を更新します。
        /// </summary>
        private static void UpdateUnitsInStock()
        {
            using (var scope = new TransactionScope(TransactionScopeOption.Required, transactionOptions))
            using (var context = new NorthwindEntities())
            {
                // 在庫個数を更新します。
                Product product = context.Products.Single(p => p.ProductID == 1);
                product.UnitsInStock += 1;
                context.SaveChanges();

                // コミットします。
                scope.Complete();
            }
        }

        /// <summary>
        /// Northwind データベースの Products テーブルの UnitsInStock の値を読み取ります。
        /// </summary>
        private static void SelectUnitsInStock()
        {
            using (var scope = new TransactionScope(TransactionScopeOption.Required, transactionOptions))
            using (var context = new NorthwindEntities())
            {
                // 在庫個数を読み取ります。
                Product product = context.Products.Single(p => p.ProductID == 1);
                short unitsInStock1 = product.UnitsInStock.Value;

                context.Refresh(RefreshMode.StoreWins, product);
                short unitsInStock2 = product.UnitsInStock.Value;

                if (unitsInStock1 != unitsInStock2)
                {
                    Console.WriteLine("{0} = {1} ({2} 回目)", unitsInStock1, unitsInStock2, ++isNonrepeatableCount);
                }
                else
                {
                    Console.WriteLine("{0} = {1}", unitsInStock1, unitsInStock2);
                }
            }
        }
    }
}


注意点
(1) ObjectContext.Refresh メソッドの引数に RefreshMode 列挙体の値を指定しなければなりませんが、
     今回の場合はクライアント側で値を変更していないため、
     StoreWins、ClientWins のうちどちらを指定しても同じ結果となります。

バージョン情報
.NET Framework 4
SQL Server 2008, 2008 R2

ノンリピータブル リードとその解決方法 その 1

(目次: トランザクションのサンプル)

ノンリピータブル リード (Non-repeatable Read) とは、
同一のトランザクション内で値を複数回読み取ったときに、その結果が異なってしまう現象のことです。
今回は、ノンリピータブル リードの発生と SQL Server における解決方法について、実際のコードを示して説明します。

前提として、SQL Server におけるロック メカニズムやトランザクション分離レベルについての基礎知識が必要になります。
これらは SQL Server のロック管理 (@IT) で確認できます。

以下で示すサンプルでは同時実行制御のために自動トランザクション (TransactionScope クラス) を使用していますが、
手動トランザクションなどを使用しても同様です。
さらに、データ アクセスのために SqlCommand クラスを使用していますが、
型指定された TableAdapter などを使用しても同様です。

■ ステップ 1: Read Committed (→ノンリピータブル リード発生)

先にコードを示します。
コンソール アプリケーション プロジェクトを作成し、Program.cs に次のように記述します。
System.Transactions.dll への参照も必要です。


using System;
using System.Data.SqlClient;
using System.Threading.Tasks;
using System.Transactions;

namespace NonrepeatableRead
{
    class Program
    {
        private const string NorthwindConnectionString = @"Data Source=.\SQLExpress;Initial Catalog=Northwind;Integrated Security=True";

        private const string SelectCommandText = "select UnitsInStock from Products where ProductID = 1";
        private const string UpdateCommandText = "update Products set UnitsInStock = UnitsInStock + 1 where ProductID = 1";

        private static readonly TransactionOptions transactionOptions = new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted };

        private static int isNonrepeatableCount;

        static void Main(string[] args)
        {
            // 更新と読み取りを並列に実行します。
            Parallel.Invoke(
                () =>
                {
                    for (int i = 0; i < 200; i++)
                    {
                        UpdateUnitsInStock();
                    }
                },
                () =>
                {
                    for (int i = 0; i < 100; i++)
                    {
                        SelectUnitsInStock();
                    }
                }
            );
        }

        /// <summary>
        /// Northwind データベースの Products テーブルの UnitsInStock の値を更新します。
        /// </summary>
        private static void UpdateUnitsInStock()
        {
            using (var scope = new TransactionScope(TransactionScopeOption.Required, transactionOptions))
            using (var connection = new SqlConnection(NorthwindConnectionString))
            {
                // データベース接続を開きます。
                connection.Open();

                // 在庫個数を更新します。
                using (var command = new SqlCommand(UpdateCommandText, connection))
                {
                    command.ExecuteNonQuery();
                }

                // コミットします。
                scope.Complete();
            }
        }

        /// <summary>
        /// Northwind データベースの Products テーブルの UnitsInStock の値を読み取ります。
        /// </summary>
        private static void SelectUnitsInStock()
        {
            using (var scope = new TransactionScope(TransactionScopeOption.Required, transactionOptions))
            using (var connection = new SqlConnection(NorthwindConnectionString))
            {
                // データベース接続を開きます。
                connection.Open();

                // 在庫個数を読み取ります。
                using (var command = new SqlCommand(SelectCommandText, connection))
                {
                    short unitsInStock1 = (short)command.ExecuteScalar();
                    short unitsInStock2 = (short)command.ExecuteScalar();

                    if (unitsInStock1 != unitsInStock2)
                    {
                        Console.WriteLine("{0} = {1} ({2} 回目)", unitsInStock1, unitsInStock2, ++isNonrepeatableCount);
                    }
                    else
                    {
                        Console.WriteLine("{0} = {1}", unitsInStock1, unitsInStock2);
                    }
                }
            }
        }
    }
}


今回も Northwind データベースの Products テーブルを使用します。
読み取りおよび更新の対象となるのはこの図の値です。
なお、UnitsInStock 列のデータ型は smallint (C# では short) です。

Products テーブルのデータ

UpdateUnitsInStock メソッドは、その在庫個数の値を 1 だけ増加させます。

SelectUnitsInStock メソッドは、その在庫個数の値を 2 回読み取って比較します。
トランザクション分離レベルを Read Committed に設定して、一連の処理を TransactionScope で囲みます。

そしてこの 2 つのメソッドを並列に実行します。
なお、並列処理には .NET Framework 4 で追加されたタスク並列ライブラリ (TPL) を使用しています。

ステップ 1 実行結果

実行結果は上の図のようになります。
SelectUnitsInStock メソッドを 100 回実行して 40 回ほどは同一トランザクション内で値が変化してしまっています。
これがノンリピータブル リードという現象です。
同一トランザクション内では、何度読み取っても同じ値にならなければ一貫性を満たしているとは言えません。

Read Committed の場合には、トランザクション内で共有ロックを最後まで取得し続けないために、
別のトランザクションから更新ができてしまいます。

■ ステップ 2: Repeatable Read (→解決)

トランザクション分離レベルを Repeatable Read に変更します。


private static readonly TransactionOptions transactionOptions = new TransactionOptions { IsolationLevel = IsolationLevel.RepeatableRead };


コードを上記のように 1 行だけ書き換えて実行します。

ステップ 2 実行結果

すると、今度は期待通りに動作します。
Repeatable Read であればトランザクションが終了するまで共有ロックを取得するため、
別のトランザクションは値を更新できなくなります。

ただし、このようにして一貫性についての解決はできるのですが、
同一トランザクション内で同じ値を複数回読み取るのは無駄に通信を発生させているともいえます。
同じ値を読み取るのが 1 回だけになるように設計するのが望ましいでしょう。
そうすれば Repeatable Read を使う必要もなく、Read Committed で十分となります。

同一トランザクション内で更新が必要な業務であれば、
ロスト アップデートとその解決方法 その 1 で説明した通り、
Read Committed で更新ロックを取得する方法を利用するとよいでしょう。

バージョン情報
.NET Framework 4
SQL Server 2008, 2008 R2

参照
.NET エンタープライズ Web アプリケーション開発技術大全 〈Vol.5〉 トランザクション設計編
エンタープライズ技術大全(トランザクション設計編) – WikiWiki (上記書籍の要約)
SQL Server のロック管理 (@IT)

ダーティ リードとその解決方法 その 2

前回のダーティ リードとその解決方法 その 1 では SqlCommand クラスを利用したコードを示しましたが、
今回は ADO.NET Entity Framework を利用したコードを示します。
ただし、特に説明する部分はないので、ステップ 2 (Read Committed) の場合のみ示します。

なお、下記のコードを記述する前に、ロスト アップデートとその解決方法 その 3 と同様に
Visual Studio で ADO.NET Entity Data Model を作成しておきます。


using System;
using System.Linq;
using System.Threading.Tasks;
using System.Transactions;

namespace DirtyRead
{
    class Program
    {
        private static readonly TransactionOptions transactionOptions = new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted };

        private static int hasStockCount;

        static void Main(string[] args)
        {
            // 更新と読み取りを並列に実行します。
            Parallel.Invoke(
                () =>
                {
                    for (int i = 0; i < 200; i++)
                    {
                        UpdateUnitsInStock();
                    }
                },
                () =>
                {
                    for (int i = 0; i < 100; i++)
                    {
                        SelectUnitsInStock();
                    }
                }
            );
        }

        /// <summary>
        /// Northwind データベースの Products テーブルの UnitsInStock の値を更新してロールバックします。
        /// </summary>
        private static void UpdateUnitsInStock()
        {
            using (var scope = new TransactionScope(TransactionScopeOption.Required, transactionOptions))
            using (var context = new NorthwindEntities())
            {
                // 在庫個数を更新します。
                Product product = context.Products.Single(p => p.ProductID == 1);
                product.UnitsInStock = 1;
                context.SaveChanges();

                // TransactionScope.Complete メソッドを呼び出さないのでロールバックされます。
            }
        }

        /// <summary>
        /// Northwind データベースの Products テーブルの UnitsInStock の値を読み取ります。
        /// </summary>
        private static void SelectUnitsInStock()
        {
            using (var scope = new TransactionScope(TransactionScopeOption.Required, transactionOptions))
            using (var context = new NorthwindEntities())
            {
                // 在庫個数を読み取ります。
                Product product = context.Products.Single(p => p.ProductID == 1);
                bool hasStock = product.UnitsInStock > 0;

                if (hasStock)
                {
                    hasStockCount++;
                    Console.WriteLine("在庫あり ({0} 回目)", hasStockCount);
                }
                else
                {
                    Console.WriteLine("在庫なし");
                }
            }
        }
    }
}


バージョン情報
.NET Framework 4
SQL Server 2008, 2008 R2

ダーティ リードとその解決方法 その 1

(目次: トランザクションのサンプル)

ダーティ リード (Dirty Read) とは、更新中でコミットされていないデータを読み取ってしまう現象のことです。
今回は、ダーティ リードの発生と SQL Server における解決方法について、実際のコードを示して説明します。

前提として、SQL Server におけるロック メカニズムやトランザクション分離レベルについての基礎知識が必要になります。
これらは SQL Server のロック管理 (@IT) で確認できます。

以下で示すサンプルでは同時実行制御のために自動トランザクション (TransactionScope クラス) を使用していますが、
手動トランザクションなどを使用しても同様です。
さらに、データ アクセスのために SqlCommand クラスを使用していますが、
型指定された TableAdapter などを使用しても同様です。

■ ステップ 1: Read Uncommitted (→ダーティ リード発生)

先にコードを示します。
コンソール アプリケーション プロジェクトを作成し、Program.cs に次のように記述します。
System.Transactions.dll への参照も必要です。


using System;
using System.Data.SqlClient;
using System.Threading.Tasks;
using System.Transactions;

namespace DirtyRead

    class Program 
    { 
        private const string NorthwindConnectionString = @"Data Source=.\SQLExpress;Initial Catalog=Northwind;Integrated Security=True"

        private const string SelectCommandText = "select UnitsInStock from Products where ProductID = 1"
        private const string UpdateCommandText = "update Products set UnitsInStock = 1 where ProductID = 1"

        private static readonly TransactionOptions transactionOptions = new TransactionOptions { IsolationLevel = IsolationLevel.ReadUncommitted }; 

        private static int hasStockCount; 

        static void Main(string[] args) 
        { 
            // 更新と読み取りを並列に実行します。 
            Parallel.Invoke( 
                () => 
                { 
                    for (int i = 0; i < 200; i++
                    { 
                        UpdateUnitsInStock(); 
                    } 
                }, 
                () => 
                { 
                    for (int i = 0; i < 100; i++
                    { 
                        SelectUnitsInStock(); 
                    } 
                } 
            ); 
        } 

        /// <summary> 
        /// Northwind データベースの Products テーブルの UnitsInStock の値を更新してロールバックします。 
        /// </summary> 
        private static void UpdateUnitsInStock() 
        { 
            using (var scope = new TransactionScope(TransactionScopeOption.Required, transactionOptions)) 
            using (var connection = new SqlConnection(NorthwindConnectionString)) 
            { 
                // データベース接続を開きます。 
                connection.Open(); 

                // 在庫個数を更新します。 
                using (var command = new SqlCommand(UpdateCommandText, connection)) 
                { 
                    command.ExecuteNonQuery(); 
                } 

                // TransactionScope.Complete メソッドを呼び出さないのでロールバックされます。 
            } 
        } 

        /// <summary> 
        /// Northwind データベースの Products テーブルの UnitsInStock の値を読み取ります。 
        /// </summary> 
        private static void SelectUnitsInStock() 
        { 
            using (var scope = new TransactionScope(TransactionScopeOption.Required, transactionOptions)) 
            using (var connection = new SqlConnection(NorthwindConnectionString)) 
            { 
                // データベース接続を開きます。 
                connection.Open(); 

                // 在庫個数を読み取ります。 
                using (var command = new SqlCommand(SelectCommandText, connection)) 
                { 
                    bool hasStock = (short)command.ExecuteScalar() > 0

                    if (hasStock) 
                    { 
                        hasStockCount++
                        Console.WriteLine("在庫あり ({0} 回目)", hasStockCount); 
                    } 
                    else 
                    { 
                        Console.WriteLine("在庫なし"); 
                    } 
                } 
            } 
        } 
    }
}


今回も Northwind データベースの Products テーブルを使用します。
読み取りおよび更新の対象となるのはこの図の値です。あらかじめ値を 0 に設定しておきます。 
なお、UnitsInStock 列のデータ型は smallint (C# では short) です。

Products テーブルのデータ

UpdateUnitsInStock メソッドは、その在庫個数の値を 1 に更新するものの、コミットせずにロールバックします。

SelectUnitsInStock メソッドは、その在庫個数の値を読み取って、在庫の有無を判定します。
トランザクション分離レベルを Read Uncommitted に設定して、一連の処理を TransactionScope で囲みます。

そしてこの 2 つのメソッドを並列に実行します。
なお、並列処理には .NET Framework 4 で追加されたタスク並列ライブラリ (TPL) を使用しています。

ステップ 1 実行結果

実行結果は上の図のようになります。
UpdateUnitsInStock メソッドの処理は実質的には無意味で在庫は増えていないはずですが、
在庫個数を 100 回読み取って 40 回ほどは「在庫あり」と判定されています (結果は毎回変化します)。
これがダーティ リードという現象です。

Read Uncommitted の場合には、データを読み取るときにロックを取得しないためにこのようなことが起こります。

■ ステップ 2: Read Committed (→解決)

トランザクション分離レベルを Read Committed に変更します。


private static readonly TransactionOptions transactionOptions = new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted };


コードを上記のように 1 行だけ書き換えて実行します。

ステップ 2 実行結果

すると、今度は期待通りに動作します。
Read Committed であればデータを読み取るときにロックを取得するため、
更新処理がコミットまたはロールバックされるまではデータを読み取ることができなくなります。

データの参照と更新の必要な業務で Read Uncommitted を指定することは慣習的にもまずないでしょう。

なお、ダーティ リードとその解決方法 その 2 に ADO.NET Entity Framework を利用したコードを載せました。

バージョン情報
.NET Framework 4
SQL Server 2008, 2008 R2

参照
.NET エンタープライズ Web アプリケーション開発技術大全 〈Vol.5〉 トランザクション設計編
エンタープライズ技術大全(トランザクション設計編) – WikiWiki (上記書籍の要約)
SQL Server のロック管理 (@IT)

ロスト アップデートとその解決方法 その 3

ロスト アップデートとその解決方法 その 1 では SqlCommand クラスを利用したコードを示しましたが、
今回は ADO.NET Entity Framework を利用したコードを示します。
ただし、ステップ 3 (Read Committed & UPDLOCK) の場合のみ示します。

Entity Framework では基本的に LINQ to Entities でクエリを記述しますが、更新ロックを取得するための構文は存在しません。
ObjectContext.ExecuteStoreQuery メソッドにより、データベースに依存する任意の SELECT 文を実行できます。

なお、下記のコードを記述する前に Visual Studio で ADO.NET Entity Data Model を作成しておきます。
名前は NorthwindModel.edmx としておきます。

NorthwindModel


using System;
using System.Linq;
using System.Threading.Tasks;
using System.Transactions;

namespace LostUpdate
{
    class Program
    {
        private static readonly TransactionOptions transactionOptions = new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted };

        static void Main(string[] args)
        {
            Parallel.For(0, 100, i => AddUnitsInStock());
        }

        private static void AddUnitsInStock()
        {
            try
            {
                short oldValue, newValue;

                using (var scope = new TransactionScope(TransactionScopeOption.Required, transactionOptions))
                using (var context = new NorthwindEntities()) 
                {
                    // 商品を取得します。
                    Product product = context.ExecuteStoreQuery<Product>("select * from Products with (updlock) where ProductID = {0}", 1).Single();
                    context.AttachTo("Products", product);

                    // 在庫個数を 1 だけ増加させます。
                    oldValue = product.UnitsInStock.Value; 
                    newValue = (short)(oldValue + 1);
                    product.UnitsInStock = newValue;

                    // 商品を更新します。
                    context.SaveChanges();

                    // コミットします。
                    scope.Complete(); 
                } 

                Console.WriteLine("{0} → {1}", oldValue, newValue); 
            }
            catch (System.Data.DataException ex) 
            {
                Console.WriteLine(ex.Message); 
            } 
        } 
    }
}


注意点
(1) ObjectContext.ExecuteStoreQuery メソッドでパラメーター化クエリを実行する場合、
     パラメーター名には順に {0}, {1}, {2}, … または @p0, @p1, @p2, … を使用します。
(2) ObjectContext.ExecuteStoreQuery メソッドを使用した場合、
     取得したデータは自動的に ObjectContext にアタッチされないため、ObjectContext.AttachTo メソッドを呼び出します。
     標準的な

          Product product = context.Products.Single(p => p.ProductID == 1);

     などのような場合には呼び出す必要はありません。
(3) UPDATE、INSERT、DELETE などの変更操作を任意の SQL で実行する場合には、
     ObjectContext.ExecuteStoreCommand メソッドを使用します。

バージョン情報
.NET Framework 4
SQL Server 2008, 2008 R2

参照
ObjectContext.ExecuteStoreQuery メソッド
ObjectContext.ExecuteStoreCommand メソッド
ストア コマンドの直接実行

ロスト アップデートとその解決方法 その 2

前回のロスト アップデートとその解決方法 その 1 では自動トランザクション (TransactionScope) を
利用したコードを示しましたが、今回は手動トランザクションを利用したコードを示します。

ただし、特に説明する部分はないので、ステップ 3 (Read Committed & UPDLOCK) の場合のみ示します。
TransactionScope クラスの代わりに SqlTransaction クラスを利用するだけです。


using System;
using System.Data;
using System.Data.SqlClient;
using System.Threading.Tasks;

namespace LostUpdate
{
    class Program
    { 
        private const string NorthwindConnectionString = @"Data Source=.\SQLExpress;Initial Catalog=Northwind;Integrated Security=True"

        private const string SelectCommandText = "select UnitsInStock from Products with (updlock) where ProductID = 1"
        private const string UpdateCommandText = "update Products set UnitsInStock = @UnitsInStock where ProductID = 1";

        static void Main(string[] args) 
        { 
            Parallel.For(0, 100, i => AddUnitsInStock()); 
        } 

        /// <summary>
        /// Northwind データベースの Products テーブルの UnitsInStock の値を 1 だけ増加させます。
        /// </summary>
        private static void AddUnitsInStock() 
        { 
            try
            { 
                short oldValue, newValue; 

                using (var connection = new SqlConnection(NorthwindConnectionString)) 
                { 
                    // データベース接続を開きます。
                    connection.Open(); 

                    // トランザクションを開始します。
                    SqlTransaction transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted); 

                    // 在庫個数を取得します。
                    using (var command = new SqlCommand(SelectCommandText, connection) { Transaction = transaction }) 
                    { 
                        oldValue = (short)command.ExecuteScalar(); 
                    } 

                    // 在庫個数を 1 だけ増加させます。
                    newValue = (short)(oldValue + 1); 

                    // 在庫個数を更新します。
                    using (var command = new SqlCommand(UpdateCommandText, connection) { Transaction = transaction }) 
                    { 
                        command.Parameters.AddWithValue("@UnitsInStock", newValue); 
                        command.ExecuteNonQuery(); 
                    } 

                    // コミットします。
                    transaction.Commit(); 
                } 

                Console.WriteLine("{0} → {1}", oldValue, newValue); 
            } 
            catch (SqlException ex) 
            { 
                Console.WriteLine(ex.Message); 
            } 
        } 
    }
}


バージョン情報
.NET Framework 4
SQL Server 2008, 2008 R2