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

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

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


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

namespace PhantomRead
{
    class Program
    {
        private static readonly TransactionOptions transactionOptions = new TransactionOptions { IsolationLevel = IsolationLevel.Serializable };

        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 context = new NorthwindEntities())
            {
                // カテゴリを追加します。
                context.AddToCategories(Category.CreateCategory(0, "New Category"));
                context.SaveChanges();

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

        /// <summary>
        /// Northwind データベースの Categories テーブルの行数を読み取ります。
        /// </summary>
        private static void SelectCategoriesCount()
        {
            using (var scope = new TransactionScope(TransactionScopeOption.Required, transactionOptions))
            using (var context = new NorthwindEntities())
            {
                // カテゴリの数を読み取ります。
                int count1 = context.Categories.Count();
                int count2 = context.Categories.Count();

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


注意点
(1) CategoryID 列には IDENTITY が指定されているため、
     新しい Category オブジェクトを作成するときに指定する CategoryID は任意の値でかまいません (上記では 0)。
     SaveChanges メソッドが呼ばれると、テーブルに行を挿入するとともに、採番された CategoryID を取得します。
     具体的には、次と等価な SQL が送信されます。

insert [Categories] ([CategoryName], [Description], [Picture]) values (N’New Category’, null, null)
select [CategoryID] from [Categories] where @@ROWCOUNT > 0 and [CategoryID] = scope_identity()

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

広告

ノンリピータブル リードとその解決方法 その 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

ダーティ リードとその解決方法 その 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

ロスト アップデートとその解決方法 その 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 メソッド
ストア コマンドの直接実行