Hatena::ブログ(Diary)

出羽ブログ RSSフィード

Seasar Conference 2008 Autumn - 9/6(SAT), Tokyo

2008-07-06

S2JDBC の弱点を補完するS2AbstractService

| 10:38 | S2JDBC の弱点を補完するS2AbstractServiceを含むブックマーク

このエントリーではSeasar 2.4.26 から 導入された S2AbstractService について書かせて頂きます。S2AbstractService を活用することで、タイプセーフを保ちつつも、データアクセスロジック関連のソースコードを大幅に減らす効果が期待できます。


S2JDBC の弱点

S2JDBCを使えば、お手軽かつパワフルにデータアクセス処理が実現できます。しかし、生のS2JDBCを野放し状態に使った場合、プロジェクトの規模が少し大きくなると、ソースコードの重複を生みやすくなる問題に直面します。具体的に、次の1件分のデータ取得処理ですら、コピー&ペーストされて複数箇所で使用されてしまいます。

Emp emp = jdbcManager.from(Emp.class).id(empId).getSingleResult();

対処方法は、共通処理を抽出してメソッド化することです。実際には、後から共通部分の抽出は大変なので、先に重複しそうな共通処理メソッドをどこかのクラスに記述することが望ましいです。最近のSeasarでは、エンティティ単位にServiceクラスを作成して、そこにデータアクセス処理を書くことが推奨されています。上記のコードはEmpエンティティに関連するので、以下のようにEmpサービスクラスに移します。(jdbcManagerはAbstractServiceクラス内に保持しているものとします。)

public EmpService extends AbstractService {
    public Emp findById(Integer id) {
        return jdbcManager.from(Emp.class).id(empId).getSingleResult();
    }
}

次に同じように部署を扱うDeptエンティティのサービスクラスを見てみましょう。

public DeptService extends AbstractService {
    public Dept findById(Integer id) {
        return jdbcManager.from(Dept.class).id(empId).getSingleResult();
    }
}

ほぼ同じコードですね。一見すると、何も問題が無いように思えますが、エンティティの種類が異なるだけでほぼ同じコードを繰り返し書かなければなりません。どうしても対処できないのであれば仕方がないのですが、良い対策が存在するのであれば何とかしたいものです。


ジェネリックDAO

先ほどのエンティティの種類が異なるだけでほぼ同じコードを繰り返し書かなければならない問題へは「ジェネリックDAO」を用いることで解決することができます。「ジェネリックDAO」により、汎用性とタイプセーフの両方を備えたDaoを実現することができます。ジェネリックDAOについては以下の記事が参考になります。

ちなみに、「DAOを繰り返すな!」の記事では、「ジェネリックDAOインターフェース」を用いています。一方、S2AbstractService では「ジェネリックDAOインターフェース」は使っていません。代わりに抽象クラスを用いてほぼ同様の内容をよりシンプルな方法で実現しています。


S2AbstractService

S2AbstractService は、この「ジェネリックDAO」の考え方が取り入れられています。S2AbstractService は S2JDBC を使ったプロジェクトに共通のサービスクラスを作成する際の雛形となるクラスです。S2AbstractService を活用することで、データアクセスロジック関連のソースコードを大幅に減らすことができます。具体的には、エンティティの種類が異なるだけでほぼ同じコードを書かなければならない作業が不要になります。また、S2AbstractService は JdbcManager のラッパーとしても機能するので、自由度の高いJdbcManager に対して、意図的に利用方法の制約を加えることも可能です。今後、S2JDBCを使ったアプリケーションを実装する場合は、S2AbstractServiceを用いたサービスクラスの採用を検討してみてはいかがでしょうか?


S2AbstractServiceの使い方

S2AbstractServiceの使い方について説明します。S2AbstractServiceクラスは抽象クラスなので単独で使われることはありません。S2AbstractServiceは、基本的にエンティティ単位で継承したクラスを作成してもらうことを想定しています。ただし、実際のプロジェクトで活用する際には、(例えばAbstractServiceという名前で)S2AbstractServiceを継承した共通のサービスクラスを作ります。そして、この共通サービスクラスを継承して、個別のエンティティ単位のサービスを作ります。詳細は下図をご覧下さい。

f:id:dewa:20080705222438j:image


S2AbstractServiceを継承したサービスクラスの責務

S2AbstractServiceを継承したサービスクラスの責務は、対応するエンティティに関連した「データアクセスロジック」と「ビジネスロジック」になります。「データアクセスロジック」が責務であるDaoクラスからインターフェースを使うお作法を取り除き、「ビジネスロジック」の責務を加えてたものがここで言う所のServiceと考えればイメージしやすいのではないでしょうか。


サンプルコード

では、ソースコードレベルで見てみましょう。まずは、S2AbstractService(抜粋)です。

public abstract S2AbstractService<T> {

    @Resource
    protected JdbcManager jdbcManager;

    protected Class<T> entityClass;

         ... 

    public AutoSelect<T> select() {
        return jdbcManager.from(entityClass);
    }

    public int update(T entity) {
        return jdbcManager.update(entity).execute();
    }
        ... 
} 

ジェネリックを多用しているために、慣れていない人は拒否反応を示すかも知れません。深く考えると実はかなり奥が深いので、最初はサンプルを参考にしつつ感覚的に対処する、でもいいと思います。


このクラスはエンティティ単位の様々なサービスクラスから継承されることを想定しています。そのエンティティクラスの型をジェネリックパラメータ T として扱うことを抑えておきましょう。また、S2AbstractServiceがJdbcManager を保持していることも重要なポイントです。


次に、S2AbstractServiceを継承した共通クラス(以下AbstractService)を見てみましょう。

public abstract AbstractService<ENTITY> extends S2AbstractService<ENTITY> {
} 

基本はたったこれだけです。AbstractServiceは、プロジェクト内のアーキテクトが作成するクラスです。プロジェクトの内部で決めたローカルルールや定番処理に対して、ジェネリック用いたメソッドをじゃんじゃん作っていきましょう。実際にAbstractServiceにメソッドを追加した例が次のとおりです。

public abstract AbstractService<ENTITY> extends S2AbstractService<ENTITY> {
    /* 1件検索。ただし、レコードが存在しなければ、独自例外をスローする */
    public ENTITY findById(Integer id) {
        if (id == null) {
            ・・・  // パラメータid のnull チェック
        }
        ENTITY entity = select().id(id).getSingleResult();
        if (entity == null) {
            throw new NoDataFoundException(); // ←プロジェクトで自作した例外
        }
        return entity;
    }
} 

上記のfindByIdメソッドは、1件検索を実現するメソッドです。ただし、通常の1件検索とは異なり、該当データが存在しない場合は nullを返すのではなく、独自で作成した例外クラスをスローするようにしています。他にも、AbstractServiceに書くべきメソッドとしては以下のようなものが想定されます。

  • 更新対象が存在しなければ、更新件数が0件ではなく、例外をスローさせる更新処理
  • 透過的に論理削除フラグが考慮される検索メソッド
  • 引数がエンティティではななく主キーで処理可能な削除処理

次にエンティティ単位で作成するサービスクラスを見てみましょう。

public EmpService extends AbstractService<Emp> { 
} 

ポイントは、extends AbstractService<Emp> の部分です。< > 内にエンティティクラスを書いておきましょう。たったこれだけの記述で、このクラスは継承元のクラス(AbstractServiceとS2AbstractService)にてジェネリックパラメータ T で記述されたメソッドを Tの代わりにEmpクラスで指定されたものとして使用することができます。


具体的にEmpService をアクション内で呼び出す場合のサンプルコードは次のようになります。

// 結果が0件の時は例外が発生する
Emp emp = empService.findById(5);

emp.name = "hoge";
empService.update(emp);

さらに、DeptService クラスを以下のように定義するだけで、

public DeptService extends AbstractService<Dept> { 
} 

次のような呼出しが可能となります。

// 結果が0件の時は例外が発生する
Dept dept = deptService.findById(5);

上記のコードから「エンティティの種類が異なるだけでほぼ同じコードを繰り返し書くこと」から開放されていることが分かりますね。


最後に

いかがでしたか?S2JDBCは単独で使用してもそれなりに強力ですが、ジェネリックDAOを用いたS2AbstractServiceを使えば、エンティティの種類が異なるだけでほぼ同じコードを書かなければならない作業が不要になります。まさに鬼に金棒ですね。タイプセーフを保ちつつも、コード記述量が激減できるって本当に素晴らしいことだと思います。


Appendix

S2AbstractServiceの公式ドキュメントおよびjavadoc

xx 2008/07/06 17:09 エンティティの結合を検索時に指定することがほとんどなので
findByIdやfindAllなんかは実運用では出番があまりないかもです。