解説編、というか。前回の記事を後で読み直してみたところ、全く意味がわからんだろうなぁ・・・と自分自身でも思い、追加解説の記事を書くことにしました。
テーブル構成とか
前提となる環境をまず説明します。
話を簡単にするために関係ある部分だけのテーブル構成としていますが、上のような掲示板やお知らせなどのコンテンツを格納する、「記事」テーブルとそれに関連したファイル情報を格納する「ファイル情報」テーブルがあるとします。
ビジネスロジック
この記事に対するロジックは
- 記事の追加
- 記事の更新
- 記事の削除
- 記事の一覧取得
といったものが必要であるとすると、上のようなビジネスロジックを処理するクラスを用意することになります。
このうち、追加処理である add メソッドだけを今回考えてみると、下のようなシーケンス図のようになります。
これは、日本語で順を追って説明すると、
- トランザクションの開始
- アップロードファイルの保存
- ファイル情報テーブルへのinsert
- 記事テーブルへのinsert
- トランザクションのコミット
という風に実行されるということです。
やりたいこと
さて、ここからが本題です。
上記処理の間で、なんらかの異常が発生した場合、例外が発生します。たとえば、ArticleDAOのinsertメソッド内で例外が発生した場合、ロールバックしなければいけません。これは、以下のようなコードがあると考えてください。
public static void add(Article article) throws HogeException { DaoFactory factory = DaoFactory.getFactory(); TransactionManager tr = new TransactionManager(); try { // トランザクション開始 tr.begin(); // ファイルの保存 FileManager.save(article.getFile()); // ファイル情報insert FileDescriptionDAO file_dao = factory.createFileDescriptionDAO(); file_dao.insert(article.getFileDescription()); // 記事insert ArticleDAO article_dao = factory.createArticleDAO(); article_dao.insert(article); // トランザクションコミット tr.commit(); } catch (Exception e) { logger.error(e.getMessage()); // 例外発生時はロールバック tr.rollback(); throw new HogeException(e); } }
上記で、例外発生時のロールバックには対応できています。それによって処理中のエラーが発生した場合データベース処理は全て元の状態に戻されます。
そこで問題は、FileManager.save() で行った保存処理をどうやって元に戻すのか?という点になります。この処理はファイルシステムの操作になるため、データベースのロールバックでは元に戻りません。
したがって、前回の記事でhottinさんにご指摘頂いたようにファイル処理を最後に持ってくれば問題は解決するのですが、複雑なファイル処理になってくるとファイル処理とファイル情報のレコード処理をひとまとめにしたロジックを用意しておいて、それを使いたくなってきます(FileDAO内でファイル処理を行ったり、FileManager内でDAOのメソッドをコールしたり...)。
そうなると、ファイルシステムの処理だけを毎回抽出して最後に持ってくるということが難しくなってきます。
解決方法
さて、プログラマがファイル処理の順序を意識することなく、コミット時にファイル処理をさせるにはどうしたらいいでしょうか。
ここから前回の記事の内容になってくるのですが、ちょっとくだけた説明をすると「実際のファイル処理を行わずファイル処理内容を記録する」ということをしてやります。実際の処理はトランザクションコミット時に一括で行います。
この、記録~再生を行うのが前回の記事ででてきたFileCommandクラスです。このサブクラスは、File.delete()とかFile.renameTo()とかの各メソッドを1つ1つクラスとして実装したものでDeleteクラス、RenameToクラスなどがあります。実際のロジックのコーディング時にはこれらを直接インスタンス化して使うのではなく、Fileクラスのサブクラスを作って、delete()やrenameTo()メソッドをオーバーライドしてその中でDeleteクラスなどのサブクラスをコールすることになります。
以下はファイルアップロード時に呼び出すメソッドの例です(この流れは全メソッド共通)。
この時点ではファイルシステム処理(実際のアップロード処理)は行いません。FileCommandManager.append()をコールして処理自体を記録しているだけです。
virtualOperation()メソッドは、「仮想的な実行」を行うものです。exists()メソッドに対応するためにFileCommandManagerクラスがstaticで保持している、追加ファイルリストと削除ファイルリストを更新します。これによって、実際には保存していないファイルに対してexists()を実行してもtrueを返すことができるようになります(exists()メソッドも、そのファイルリストを参照するようにオーバーライドしてあります)。
test()メソッドは、もし実行した場合に返される値を返すメソッドです。たとえば、存在しないパスに対してmkdir()を実行した場合はfalseを返します。この結果の算出にも先ほどの追加・削除ファイルリストを使用します。
また、FileCommandManagerにもtest()メソッドがあり追加したコマンドを順次テストしていくことができます。実際の実行処理は、TransactionManager.commit()内に、FileCommandManager.execute()を入れておけば自動的にコミットの直前にファイル処理をさせることができるようになります。
コードの例
実際にこれを使った場合のコードの例を見てみましょう。分かりやすいように、delete()メソッドを例にとります。FileDescriptionDAOのdelete()メソッドでファイル自体も削除する処理が入っているという前提で見てください。
まず、普通のFileクラスを使った場合。
public delete(FileDescription desc) throws HogeException { File file = new File(desc.getFilePath()); if(!file.delete()) throw new HogeException("file cannot delete"); doDelete(desc); }
次に、前述のDeleteCommandをコールするLazyFileクラスを使った場合。
public delete(FileDescription desc) throws HogeException { File file = new LazyFile(desc.getFilePath()); if(!file.delete()) throw new HogeException("file cannot delete"); doDelete(desc); }
このように、かなりの低コストで導入できることが分かります。Commandクラスやそれをコールするクラスを実装するという手間はありますが、個人的にはなかなか便利なんじゃ~ないかなと思います。
と、いろいろ書きましたがやっぱり分かりにくいですよね、これ・・・。orz