Windows Phone で既存のデータベース ファイルを使う

(Windows Phone Advent Calendar 2011 の 18 日目です。)

Windows Phone 7.5 アプリケーションでは、ローカル データベースとして SQL Server Compact を利用できます。
データベース ファイルの拡張子は .sdf です。
アプリケーションから接続するには LINQ to SQL を利用します。

今回は、Database First で既存のデータベース ファイル (.sdf) からコードを自動生成し、
Windows Phone 7.5 アプリケーションで動作させるまでの一連の手順について紹介します。

ちなみに、MSDN ライブラリの Windows Phone のローカル データベースの概要を見ると、
Code First な方法は載っているのですが Database First な方法は載っていません。
なお、Code First な方法についての日本語の解説は、
はぢめてのWindows Phone 7でのデータベース(Linq to Sql) などを参照するとよいでしょう。

■ データベース ファイルを準備する

「既存のデータベース ファイル (.sdf)」と書きましたが、
これまでに Windows Phone プラットフォーム以外で使用されていたものや、新規に作成したものも含みます。

ただし、方法: Windows Phone アプリケーションと共に参照データベースを展開する
「重要な注」に記述されている通り、デスクトップで作成された SQL Server Compact データベースを
Windows Phone アプリケーションで動作させることは公式にサポートされていません

したがって、自己責任で利用しましょう。

以下に、SQL Server Compact のデータベース ファイルを新規に作成する方法を簡単に示します。

(1) SQL Server Management Studio を使う場合

[サーバーへの接続] ダイアログの [サーバーの種類] で SQL Server Compact を選択し、
[データベース ファイル] で <新しいデータベース…> を選択します。
すると別のダイアログが表示されるので、ファイル名を指定します。

SQL Server Management Studio でデータベース ファイルを作成

あとはこのデータベース ファイルに接続してテーブルを定義します。
ただし、SQL Server の場合とは異なり、データベース ダイアグラムは利用できません。

SQL Server Management Studio でデータを作成

(2) Visual Studio を使う場合

Windows Phone 系のプロジェクトからでは作成できません。
Windows アプリケーション系のプロジェクトから次の図のように作成します。

Visual Studio でデータベース ファイルを作成

あとはサーバー エクスプローラーからデータベース ファイルに接続してテーブルを定義します。
コンテキスト メニューの [テーブル データの表示] からデータをグリッド形式で操作することもできます。
SQL を利用することもできます。

Visual Studio でデータを作成

さて、初期データを外部から一括で取り込みたい場合についてですが、
SQL Server Compact データベースには bcp ユーティリティでデータを一括でインポートできません。
次のような方法が考えられます。

  • Integration Services を利用する (未検証)
  • 後述する Proxy コードを生成してからコンソール アプリケーションを作成してデータを投入する
  • INSERT 文を生成して Management Studio や Visual Studio で実行する

■ データベース ファイルからコードを生成する

SQL Server などのデータベースを対象とした開発では、
Visual Studio で既存のデータベースから Proxy クラスを自動生成するという方法がよく用いられています。
しかし残念ながら、Visual Studio には .sdf ファイルから LINQ to SQL 用のコードを自動生成する機能がありません。

そこで、SqlMetal.exe というコマンドライン ツールを利用します。
これを使うと、.sdf ファイルから LINQ to SQL 用のコードを自動生成できます。
SqlMetal.exe は Windows SDK に含まれています (Visual Studio とともにインストールされます)。

例えば、次に示すようなバッチ ファイルを作成します。
SqlMetal.exe の引数には、.sdf ファイルのパス、作成されるコード ファイルのパス、
型指定された DataContext の名前空間とクラス名を指定します。絶対パスおよび相対パスが使えます。

CreateDataContext.bat


rem 64 bit OS の場合
set SqlMetal="C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Bin\SqlMetal.exe"
%SqlMetal% Test.sdf /code:TestDataContext.cs /namespace:PhoneApp1 /context:TestDataContext /pluralize


このバッチ ファイルを実行すると TestDataContext.cs が生成されます。
ただし、本来これは .NET Framework 用のコードであり、Windows Phone 用のプロジェクトでは
型指定された DataContext クラスの一部のコンストラクターがコンパイル エラーとなるので、それらを手動で削除します。

TestDataContext.cs


// ・・・ (省略) ・・・
public partial class TestDataContext : System.Data.Linq.DataContext
{
// ・・・ (省略) ・・・

    public TestDataContext(string connection) :
            base(connection, mappingSource)
    {
        OnCreated();
    }

/* IDbConnection は存在しません。
    public TestDataContext(System.Data.IDbConnection connection) :
            base(connection, mappingSource)
    {
        OnCreated();
    }
*/

    public TestDataContext(string connection, System.Data.Linq.Mapping.MappingSource mappingSource) :
            base(connection, mappingSource)
    {
        OnCreated();
    }

/* IDbConnection は存在しません。
    public TestDataContext(System.Data.IDbConnection connection, System.Data.Linq.Mapping.MappingSource mappingSource) :
            base(connection, mappingSource)
    {
        OnCreated();
    }
*/

    public System.Data.Linq.Table<Company> Companies
    {
        get
        {
            return this.GetTable<Company>();
        }
    }
        
    public System.Data.Linq.Table<Line> Lines
    {
        get
        {
            return this.GetTable<Line>();
        }
    }

// ・・・ (省略) ・・・


その他の注意点
(1) Visual Studio コマンド プロンプトを利用すれば、SqlMetal.exe の絶対パスは必要ありません。
(2) このコードは、同じテーブル構造を持った SQL Server などに接続する場合にも利用できます。
     コンストラクターの引数に渡す接続文字列を変えるだけです。
(3) 引数に /pluralize を付加すると、単語の単数形・複数形を適切に判断してコードを生成します。
     上記の例では、Companies テーブルのエンティティの名前が Company になっています。

■ データベース ファイルを配置する

上記で準備したデータベース ファイル (.sdf) とコード ファイル (.cs) を Windows Phone アプリケーションの
プロジェクトに追加します (自動的にデータベース ファイルの [ビルド アクション] がコンテンツ、
コード ファイルの [ビルド アクション] がコンパイルになるはずです)。
また、プロジェクトの参照設定に System.Data.Linq を追加します。

初回実行時にこのデータベース ファイルを分離ストレージに配置するために、
アプリケーションの Launching イベントの処理を次のように追加します。
なお、接続文字列が特殊なので注意が必要です。isostore:/ のあとに分離ストレージ内でのパスを指定します。

App.xaml.cs


    // ・・・ (省略) ・・・

    private static readonly Uri TestContentUri = new Uri("Test.sdf", UriKind.Relative);
    private const string TestStoragePath = "Test.sdf";
    private const string TestConnectionString = "Data Source=isostore:/Test.sdf";

    // (たとえば、[スタート] メニューから) アプリケーションが起動するときに実行されるコード
    // このコードは、アプリケーションが再アクティブ化済みの場合には実行されません
    private void Application_Launching(object sender, LaunchingEventArgs e)
    {
        using (var context = new TestDataContext(TestConnectionString))
        {
            if (!context.DatabaseExists())
            {
                using (var input = Application.GetResourceStream(TestContentUri).Stream)
                using (var storage = IsolatedStorageFile.GetUserStoreForApplication())
                using (var output = storage.CreateFile(TestStoragePath))
                {
                    input.CopyTo(output);
                }
            }
        }
    }

    // ・・・ (省略) ・・・


■ データベース ファイルに接続する

ここまでできれば、もう難しいことはありません。
データベースに接続してデータを操作するには LINQ to SQL を利用します。


    private const string TestConnectionString = "Data Source=isostore:/Test.sdf";

    public static Company[] GetCompanies()
    {
        using (var context = new TestDataContext(TestConnectionString))
        {
            return context.Companies.ToArray();
        }
    }


バージョン情報
Windows Phone OS 7.1
Visual Studio 2010 SP1

参照
Windows Phone のローカル データベースの概要 (MSDN)
方法: Windows Phone アプリケーションと共に参照データベースを展開する (MSDN)
SqlMetal.exe (コード生成ツール) (MSDN)
Using SqlMetal to generate Windows Phone Mango Local Database classes (WindowsPhoneGeek)
はぢめてのWindows Phone 7でのデータベース(Linq to Sql) (neue cc)
Local Database for Windows Phone (雑記 – otherwise)

ファントム リードとその解決方法 その 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

ファントム リードとその解決方法 その 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)