5.トランザクション処理

初めに

 データベースを扱う上で、難しい技術の一つがトランザクション処理だと思います。また、今回のプログラムでは Entity Framework は使用しないことに決めました。 Entity Framework の代わりとして、自分で設計・実装した クラスライブラリ を使用します。 その独自クラスとうまく整合性のとれるトランザクション処理を実現する必要があります。

 結論から言いますと、トランザクションは標準クラスである TransactionScope を使用します。このクラスは分散トランザクションの機能を提供してくれます。 接続単位のトランザクションを利用することと比べると、設計や実装が格段に楽になります。そのため、 ADO.NET の標準クラスを利用しているのであれば、この章で記述したトランザクションは どのプログラムでも利用可能と思います。

 そこで問題は、どのようなやり方で TransactionScope の機能を利用するのかということになります。この問題に対する目標は、次のものになります。

  • トランザクションの実現方法を隠ぺいすること
    TransactionScope を利用していることをカプセル化して、全く意識させないこととします
  • TransactionScope が持つ自由度を制限しないこと
    トランザクションに参加している個々のデータベース処理は、それぞれ単独の処理として記述できることとします
  • 汎用ライブラリとすること
    Web Form や MVC のプログラムからいつでも利用可能な汎用ライブラリとすることとします

 こうした目標を満たす方法は、Web Form と MVC とで異なるやり方として実現してあります。以下に、その実現方法を議論しましょう。

Web Form の場合

 Web Form プログラムの場合、処理の中心場所を提供するのは System.Web.UI.Page クラスです。 新しい画面が必要になった場合には、このクラスから派生クラスを定義します。ボタンクリックなどのイベント処理もその派生クラスに定義します。

 そこで、 TransactionScope の機能を組み込むには、次のようなクラス階層がふさわしいと考えました。

					
          System.Web.UI.Page
              └ TransasctionPage
                 └ BasePage
					
					

 ここで、 TransactionPage クラスはトランザクション機能をもつ特殊な Page クラスとなります。このクラスを取り出してライブラリ化します。 すべての Web Form プログラムでは、 TransactionPage クラスの派生クラスとなるように、そのライブラリを組み込んで利用することとします。
 BasePage クラスはそのライブラリには含めません。今回のプログラムの全ての画面の親クラスとなり、 今回のプログラムにのみ必要となる情報を管理する役割を持ちます。 BasePage クラスとさらに BasePage クラスから派生した個別の画面に対応したクラスが、 プロジェクトの構成要素となります。

 TransactionPage クラスの実装を示します。

       				
1    public class TransasctionPage : Page
2    {
3        public void Execute()
4        {
5            try
6            {
7                using (TransactionScope ts = new TransactionScope())
8                {
9                    DoAction();

10                   ts.Complete();
11               }
12           }
13           catch (System.Exception ex)
14           {
15               OccurredError(ex);
16           }
17       }

18       public virtual void DoAction() { }

19       public virtual void OccurredError(Exception ex)
20       {
21       }
22   }
     
	       			

 3行目:トランザクションは Execute() メソッドの呼び出しで開始します
 7行目: TransactionScope を開始します
 9行目:トランザクションの本体処理を呼び出します。派生クラスで DoAction() メソッドを再定義してください
 10行目:トランザクション成功時にコミットします。ロールバックするには、 DoAction() メソッドの中で例外を投げます
 13行目:例外発生時にロールバックします
 15行名:例外発生時の処理を呼び出します。派生クラスで OccurredError() メソッドを再定義してください
 18行目: DoAction() の仮想関数。トランザクション処理の本体ですから、派生クラスで必ず再定義してください。
  ルールは一つだけです。必要なデータベース処理を順次行います。全体をロールバックしたいときだけ、処理の途中で例外を投げてください
 19~21行目:OccurredError() の仮想関数。独自のエラー処理が必要な場合に、派生クラスで適宜再定義してください

  DoAction() メソッドの実装例は、 こちら をご覧ください。

MVC の場合

 Web MVC プログラムの場合、処理の中心場所を提供するのは System.Web.Mvc.Controller クラスです。 URL から起動されるコントローラクラスを、このクラスから派生することから作業が始まります。

 そこで、 TransactionScope の機能を組み込むには、次のようなクラス階層がふさわしいと考えました。

					
          System.Web.Mvc.Controller
              └ TransactionController
                 └ FieldworkController
					
					

 ここで、 TransactionController クラスはトランザクション機能をもつ特殊な Controller クラスとなります。このクラスを取り出してライブラリ化します。 すべての Web MVC プログラムでは、 TransactionContoroller クラスから派生したコントローラを使用するために、そのライブラリを組み込んで利用します。
 FieldworkController クラスはそのライブラリには含めません。今回のプログラムの全てのコントローラの親クラスとなり、 今回のプログラムにのみ必要となる情報を管理する役割を持ちます。 FieldworkController クラスと個別の画面に対応した派生クラスが、 プロジェクトの構成要素となります。

 TransactionController クラスの実装を示します。

      				
1   public class TransactionController : Controller
2   {
3       public void Execute()
4       {
5           try
6           {
7               using (TransactionScope ts = new TransactionScope())
8               {
9                   DoAction();

10                  ts.Complete();
11              }
12          }
13          catch (System.Exception ex)
14          {
15              OccurredError(ex);
16          }
17      }

18      public virtual void DoAction() { }

19      public virtual void OccurredError(Exception ex)
20      {
21          ModelState.AddModelError("dbError", ex.Message);
22      }
23  }
    
           			

 ご覧のように、 Web Form の実装と同じです。詳細な説明は、 Web Form の章を参照してください。
 違っているのは、21行目でデフォルトの OccurredError() メソッドを実装してあることだけです。独自のエラー処理が必要かどうかに応じて、 FieldworkController または個別のコントローラクラスで再定義してください。
 デフォルトのエラー処理内容に関しては、 こちら を参照してください。

  DoAction() メソッドの実装例は、 こちら をご覧ください。

共通の解決方法

 これまでの章では、 WEB Form と MVC とで、トランザクションの実装方法を変えています。これは、トランザクションの機能は、処理の中心となるクラスに実装しようと考えたからです。
 なぜそのようにしたのか?といえば、エラー処理のしやすさを考慮したためです。何らかのエラーが発生した場合には、ブラウザに対してそのエラー内容を返す必要があります。その場合、処理の中心となるクラスにトランザクション処理を実装すれば、 そのエラー処理自体は、派生したクラスにすなおに実装できます。また、その実装方法は親クラスのメソッドをオーバーライドすることで可能になります。

 しかしながら、トランザクション処理を WEB Form と MVC とで共通にすることも可能です。以下の章では、トランザクション処理を共通化するための方法を議論していきます。

共通化のためのクラスとインターフェース

 共通化のためのクラスとインターフェースはごく単純です。インターフェースが1つとクラスが2つです。まずインターフェースを紹介します。

					
1   public interface IService
2   {
3       void Execute();
4   }
					
					

 引数なし、戻り値なしのメソッド Execute() を定義してあるだけの単純なものです。次は、2つのクラスを示します。

 TransactionScope の機能を組み込んであるクラス ServiceChain クラスです。このクラスはトランザクションの機能を実現する機能と、複数のサービスを管理する機能を持っています。個々のサービスが実際のデータベースを更新する処理を行い、 複数のサービス全体を1つのトランザクションとして実現します。いずれかのサービスが例外を投げた場合、トランザクションはロールバックされますし、すべてのサービス処理が完了すると、トランザクションがコミットされます。

					
1     public class ServiceChain
2     {
3         private Action<Exception> action;

4         public String Result
5         {
6             get; protected set;
7         }

8         protected List<IService> services;

9         public ServiceChain()
10        {
11            this.action = OccurredError;
12            services = new List<IService>();
13        }

14        public ServiceChain(Action<Exception> action)
15        {
16            this.action = action;
17            services = new List<IService>();
18        }

19        public int Count() => services.Count;

20        public IService At(int index) => services.ElementAt(index);

21        public void Add(IService obj) => services.Add(obj);

22        public void Remove(int index) => services.RemoveAt(index);

23        public void Remove(IService obj) => services.Remove(obj);

24        public void RemoveAll() => Clear();

25        public virtual void Clear()
26        {
27            Result = string.Empty;
28            services = new List<IService>();
29        }

30        public virtual bool Execute()
31        {
32            try
33            {
34                using (TransactionScope ts = new TransactionScope())
35                {
36                    foreach (var s in services)
37                    {
38                        s.Execute();
39                    }

40                    ts.Complete();

41                    return true;
42                }
43            }
44            catch (System.Exception ex)
45            {
46                if (action != null)
47                    action(ex);
48            }

49            return false;
50        }

51        public virtual void OccurredError(Exception ex)
52        {
53            OccurredError(ex.Message);
54        }

55        public virtual void OccurredError(String msg)
56        {
57            Result = msg;
58        }
59    }			
					
					

 3行目:例外が起こった場合に呼び出される Action 型のデリゲイトです
 4~7行目:トランザクション失敗の理由用のプロパティです
 8行目:複数のサービスを管理するためリストです
 9~13行目:デフォルトコンストラクタ。例外時の処理メソッドを初期しています
 14~18行目:コンストラクタ。例外時の処理メソッドを引数で渡しています
 19~29行目:サービスのリストを操作するためのプロパティやメソッドです
 30行目:トランザクション開始のメソッドです
 36~39行目:複数のサービスを順次処理しています
 40行目:すべてのサービスが終了した場合、トランザクションをコミットします
 44~48行目:例外が発生した場合の処理です
 47行目:例外時の処理メソッドの呼び出しです
 51~58行目:デフォルトの例外処理メソッドです

 次のクラスは、Service です。このクラスがデータベースに対する CRUD 処理を行うことになります。特に、トランザクションを構成する INSERT, UPDATE, DELETE を管理することになります。
 処理が失敗した場合、例外を投げることで、トランザクションをロールバックさせます。

					
1     public class Service : IService
2     {
3         public Service() { }

4         public virtual void Execute()
5         {
6             throw new ApplicationException("サブクラスで本体を実装してください。");
7         }
8     }
					
					

 1行目:クラス定義。 IService インターフェースを実装します
 4~7行目:トランザクションを構成するデータベース操作の本体となります
 6行目:デフォルトでは例外を投げます(つまり、トランザクションを失敗させます)

 とても簡単な仕組みですから、理解は容易と思います。

MVC での実装方法

 前章で述べた仕組みを使用すれば、WEB Form や MVC で実装したトランザクションの仕組みも、ほぼ同様に実現することができます。 以下では、 MVC におけるトランザクションの仕組みを、前章で説明した IService インターフェースと ServiceChain クラスを利用することで実装してみましょう。

 まずは復習です。 MVC でのトランザクションの実装方法は、 System.Web.Mvc.Controller クラスからトランザクションを管理するためのクラス TransactionController を派生させます。 そして、個々の画面で使用するコントローラクラスは、この TransactionController クラスから派生するというものです。

 ここでは、MyController クラスにトランザクションの機能を実装してみましょう。

           			
1   public class MyController : Controller, IService
2   {
3      private ServiceChain sc;

4      public MyController()
5      {
6         sc = new ServiceChain();
7         sc.Add(this);
8      }
		
9       private void Run()
10      {
11         sc.Execute();
12      }
		
13      public virtual void Execute()
14      {
15         // データベース処理の本体
16      }
17   }   			
           			
           			

 1行目:クラス定義。 IService インターフェースを実装します
 3行目:トランザクションを実装している ServiceChain のインスタンスを定義します
 4~8行目:コンストラクタです
 6行目:ServiceChain クラスのインスタンスを生成します
 7行目:自分自身をサービスであるかのように、サービスチェーンに追加します
 9~12行目:トランザクションを開始するメソッド名を Run() とします。他から呼び出される必要はありませんから、private としています
 11行目:トランザクションを開始します
 13~16行目:データベース処理の本体を記述します

 ここでの肝は、トランザクションの機能を実装している ServiceChain を自身のインスタンス変数としてもち、かつ自分自身がデータベース処理を行うサービスの一種としているところになります。 このやり方を応用すれば、 Windows Form でもコンソールアプリケーションでも利用することができます。