2012 11 27

フレームワークを作るの6

昨日の続きでサービス層の改善。 検索結果を返すだけなのに commit って流石にどうかと思うので、まずはこちらから片付けて行く。

対策の根幹ははっきりしていて、検索系ならトランザクションの制御をしなければいいのだな。 問題は、そのメソッドが検索系なのか更新系なのかを知る/知らせる方法。

使う側の立場から楽なのは、自分で認識してくれること。 使うDAOのメソッドが検索系なのか更新系なのかを判断して、更新系なら commit か rollback かする。

いや、判断するってのはちょっと無理がありそうだな。 現状はDAOもserviceも同じDB接続の参照を渡しているのだから、こいつをラッピングしたオブジェクトに更新系のメソッドが呼ばれたかどうかのフラグを持たせるのはどうだろう。 DAOの更新系メソッドは実行時にフラグを立てて、サービスのメソッドはフラグが立っていればトランザクションの制御をする。

って、違うだろ、俺。 サービス層にトランザクション制御を書きたくないからデータアクセス層に書くって本末転倒もいいところだろ。 そもそもの役割分担として、トランザクション制御はサービス層の役割としたのだから。 それに、このやり方にすると、サービス層のクラスのコードを見ただけでは、これがトランザクション制御をするかどうかが判らないのも問題。

ということで再び考える。

命名規則を持ち込むか。 更新処理は突き詰めれば insert か update か delete だから、トランザクション制御が必要なメソッドはこれらの文字列から始まるように名前を付けるとか。

でも、これもなぁ…。 saveProfiles みたいな名前を付ける方が自然な場合とかたくさんありそうだし。 と言うか、そもそも個別のテーブルへのアクセスから離れたレベルの処理を実装しようとしているのに、メソッド名が個々のテーブル操作を連想させる名前ってのが駄目だろ。 それに、メソッド名でトランザクション制御の有無を示すってのも、なんか中途半端に明示的で駄目な気がする。

と、色々考えて、結局アノテーションにすることにした。 インターフェース側で、メソッドに @transaction なんて宣言されてると、トランザクション制御をする。 判り易くていい感じ。

で、まずはアノテーションの定義。

package framework.service; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Transaction { }

名前はそのまま Transaction とした。 単なるマーカーなので何も持たない。

そしてアノテーションに対する処理。 プロキシの中核である TransactionInvocationHandler で、このアノテーションがあるかどうかを判断し、あればトランザクション制御の対象とする。

package framework.service; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.sql.Connection; public class TransactionInvocationHandler implements InvocationHandler { private Connection conn; private Object serviceImpl; public TransactionInvocationHandler(Connection conn, final Object serviceImpl) { this.conn = conn; this.serviceImpl = serviceImpl; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (method.isAnnotationPresent(Transaction.class)) { return invokeTransaction(method, args); } else { return method.invoke(serviceImpl, args); } } private Object invokeTransaction(Method method, Object[] args) throws Throwable { Object ret = null; try { ret = method.invoke(serviceImpl, args); conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } return ret; } }

トランザクション制御の要否で処理をばっさり分けている。 制御が必要な方は下請けメソッドに移譲し、不要な方はただサービスのメソッドを実行するだけ。

トランザクション制御をしない方だが、安全のために無条件に rollback した方がいいのだろうか。 と言うのも、更新するつもりなのにアノテーションを付け忘ていて、続けて呼ばれた次のサービスのメソッドで commit して、予想外のデータが更新されるって場合が発生しそうだから。

まあ、余計なお世話か。 単体試験レベルの問題の解決漏れをフレームワークが心配するってのも変な話だよな。

アノテーションを適用した BooksService インターフェースは次の通り。

package service; import java.sql.SQLException; import java.util.List; import dto.BookDto; import framework.exception.BusinessException; import framework.service.Transaction; public interface BooksService { @Transaction public void insertBook(final BookDto book) throws SQLException; public BookDto selectBook(final String isbn) throws SQLException; public List<BookDto> selectAllBooks() throws SQLException; @Transaction public void updateBook(final BookDto book) throws SQLException, BusinessException; @Transaction public void deleteBook(final BookDto book) throws SQLException, BusinessException; }

BooksServiceImpl は変更無し。

と思ったのだが、ここまで来てまた後戻り。 さっきは暗黙に rollback するのは不要と思ったのだが、暗黙じゃなく意図して rollback したい場合もあるんだよな。 例えば中間テーブルにデータを構築するような場合。 削除してから commit しても結果は同じだが、正常終了時に rollback してくれる方が楽でいい。

ということで、アノテーションを、 @Commit と @Rollback の2つ用意して、後者は rollback 専用とすることにした。 あと、せっかく2つに増えたのだから、パッケージも framework.annotation を新たに作って、そこに置くことにする。

まずは commit 用のアノテーション。 さっきの Transaction の名前を変えただけ。

package framework.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Commit { }

そして rollback 用のアノテーション。 これまた違いは名前だけ。

package framework.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Rollback { }

これらを処理できるように TransactionInvocationHandler も変更する。

package framework.service; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.sql.Connection; import framework.annotation.Commit; import framework.annotation.Rollback; public class TransactionInvocationHandler implements InvocationHandler { private Connection conn; private Object serviceImpl; public TransactionInvocationHandler(Connection conn, final Object serviceImpl) { this.conn = conn; this.serviceImpl = serviceImpl; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (method.isAnnotationPresent(Commit.class)) { return invokeCommit(method, args); } else if (method.isAnnotationPresent(Rollback.class)) { return invokeRollback(method, args); } else { return method.invoke(serviceImpl, args); } } private Object invokeCommit(Method method, Object[] args) throws Throwable { Object ret = null; try { ret = method.invoke(serviceImpl, args); conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } return ret; } private Object invokeRollback(Method method, Object[] args) throws Throwable { try { return method.invoke(serviceImpl, args); } finally { conn.rollback(); } } }

BooksService インターフェースの定義も、 @Transaction を @Commit に変更。

package service; import java.sql.SQLException; import java.util.List; import dto.BookDto; import framework.annotation.Commit; import framework.exception.BusinessException; public interface BooksService { @Commit public void insertBook(final BookDto book) throws SQLException; public BookDto selectBook(final String isbn) throws SQLException; public List<BookDto> selectAllBooks() throws SQLException; @Commit public void updateBook(final BookDto book) throws SQLException, BusinessException; @Commit public void deleteBook(final BookDto book) throws SQLException, BusinessException; }

わざわざ rollback 用のアノテーションを追加したのに、サンプルにそれが無いってどうよ。

現在の構成は以下の通り。

apl Main.java service BooksService.java impl BooksServiceImpl.java dao BooksDao.java dto BookDto.java util DbUtil.java framework annotation Commit.java Rollback.java service ServiceFactory.java TransactionInvocationHandler.java dao CmnDao.java DtoConverter.java SqlDefLoader.java SqlDef.java exception BusinessException.java SystemException.java sqlDef.xml

ここまでで無駄なトランザクション制御は無くなり、正常終了時に rollback したい場合にも対応できるようになった。 次はサービスのファクトリーに実装クラスを渡しているところを、どうにか出来ないかを考える。