続・SAStruts + S2JDBCのアーキテクチャ

ここで疑問点があります。この疑問点のため、眠れなくて早く起きてこのエントリを書いています。笑


ビジネスロジックをEntityとServiceに書く設計(最近流行のDDDの設計)だと思いますが、Entityのメソッドには、insertとかupdateとかdelete、かつエンティティ独自の振る舞いを持たせるServiceのメソッドには、findAllとかfindByNameとかというものが用意される認識で良いのか?


S2Daoを使用していた時は、1画面につき1Dtoを作って、そのDtoを画面表示に使っていました。


S2JDBCを使用すると、関連先のEntityが対象Entityにくっついて検索されるので、Entityを画面表示に使っても良い?


画面表示用のDtoは不要?

以下に、自分なり回答をさせて頂きますので、参考までにどうぞ。


まず、Entityのメソッドには、insertとかupdateとかdeleteのようなメソッドは持たさないです。Entityに持たすメソッドとしては、導出項目用のgetterメソッドを想定しています。

具体的には、売上明細エンティティに1つのプロパティとして振る舞わせるgetterメソッドを用意したりします。こうしておくと、売上明細エンティティのListを使って、売上明細一覧の画面表示処理がとても楽になります。

/* [商品].[単価] × [売上明細].[数量] */
public Integer getAmount(){
    return item.unitPrice * quantity;
}

ユースケース単位で用意したServiceには、ユースケース固有のfindAllとかfindByNameのメソッドを置きます。ただし、頻繁に使う共通的なメソッドについては、予め用意しておいたServiceの共通親クラスに抽象化したメソッドを使うようにします。

Serviceの共通親クラスはこんなイメージです。(ちょっと言葉は違っていますが、例のアーキテクチャの図で、「共通親クラスのジェネリックDaoにJdbcManagerを保持」と書いていた箇所に相当します。)

package tutorial.service.common;

import java.util.List;
import org.seasar.extension.jdbc.JdbcManager;
import org.seasar.framework.beans.util.BeanMap;

public abstract class AbstractService {

    public JdbcManager jdbcManager;
    
    public <E> int insert(E x) {
        return jdbcManager.insert(x).execute();
    }

    public <E> int update(E x) {        
        return jdbcManager.update(x).execute();
    }
    
    public <E> int delete(E x) {
        // TODO 削除対象が存在しなかった時のエラーハンドリングが必要であればここに書く。
        return jdbcManager.delete(x).ignoreVersion().execute();
    }

    public <E> E find(Class<E> clazz, Integer id) {
        // TODO 照会対象が存在しなかった時のエラーハンドリングが必要であればここに書く。
        return jdbcManager.from(clazz).id(id).getSingleResult();
    }    

    public <E> List<E> findAll(Class<E> clazz) {
        return jdbcManager.from(clazz).getResultList();
    }

    public <E> List<E> findAll(Class<E> clazz, String orderBy) {
        return jdbcManager.from(clazz).orderBy(orderBy).getResultList();
    }

    public <E> List<E> findByBeanMap(Class<E> clazz, BeanMap conditions,
            String leftOuterJoin, String orderBy) {
        return jdbcManager.from(clazz)
            .leftOuterJoin(leftOuterJoin)
                .where(conditions)
            .orderBy(orderBy)
            .getResultList();
    }
    
    public <E> List<E> findBySqlFile(Class<E> clazz, String sqlPath, Object parameters) {
        return jdbcManager.selectBySqlFile(clazz, sqlPath, parameters).getResultList();
    }

    public <E> long count(Class<E> clazz, BeanMap conditions) {
        return jdbcManager.from(clazz).where(conditions).getCount();
    }
    
    public boolean exist(Class<?> clazz, Integer id) {
        Object obj = jdbcManager.from(clazz).id(id).getSingleResult();
        return obj == null ? false : true;
    }
}


ジェネリックを多用しているので、面食らう人も多いと思いますが、呼び出し側はとてもシンプルです。たとえば、hogeという名前のユースケースのアクションから上記のServiceを呼び出すイメージは以下のようになります。

List<Department> deptItems = hogeService.findAll(Department.class);	

Employee employee = new Employee();
employee.name = "xxx";
...
hogeService.insert(employee);

List<Employee> employees = hogeService.findAllEmployee(hogeDto);


ちなみに、HogeServiceの実装イメージはこんな感じ。

public class HogeService extends AbstractService {
    
    public List<Employee> findAllEmployee(HogeDto dto) {
        return jdbcManager.from(Employee.class)
            .leftOuterJoin("department")
            .where(new SimpleWhere().excludesWhitespace()
                .starts("name", dto.cond_name_STARTS)
                .ge("salary", dto.cond_salary_GE)
                .le("salary", dto.cond_salary_LE)
                .eq("departmentId", dto.cond_departmentId_EQ))
            .orderBy("id")
            .getResultList();
    }    
}

注目は、AbstractService を継承している箇所です。ユースケース固有のfindAllEmployeeメソッド以外は、親クラスに定義されてるジェネリックなメソッドを使って処理していることがポイントです。

このように、いちいちエンティティごとに典型的なデータアクセス処理を書かないので、ジェネリックを上手く利用すると、コードの記述量が激減します。事前のチェック処理なんかも、(後追いでも)共通処理に含めてしまえるのも大きな利点です。


ユースケース固有のデータアクセス処理は、ユースケース粒度のServiceに記述して、それ以外の典型的なデータアクセスには共通親Serviceのメソッドを使います。この共通親Serviceは、プロジェクトの最初にガツッと用意しておいて、その後、プロジェクトの最中でちょろちょろ改善していくことになるかと思います。

画面表示用Dtoについてですが、おおまかに『単一レコード処理』と『複数レコード処理(いわゆる繰り返し)』に分けて考えます。

『単一レコード処理』については、エンティティをそのまま使うことはせずに、アクションかアクションフォームに画面項目用のプロパティを用意します。これらのプロパティの値には、エンティティの値を詰め替えたものをセットします。エンティティをそのまま使わない理由としては、ユースケース単位でのプロパティの使い回しを前提に、更新処理を考慮すると、バリデータが必要となるからです。「画面ごとにバリデータ要求が変ること」と「バリデータは画面周りと相性の良いデータ型(String)を使う」を考慮すると、単一レコード処理においては、画面項目用のデータとエンティティは分離しておいたほうが、複雑なケースでハマらないと思います。

『複数レコード処理(繰り返し)』については、更新処理を仕様的に採用するケースが比較的少ないので、エンティティのリストを使って画面を組み立てるようにします。前述した導出項目プロパティ(実際にはGetter)がエンティティに定義してあると、とてもコーディングが楽になったりします。導出項目プロパティや関連エンティティを駆使しての一覧表示が不可能ではないが複雑になるケースにおいては、繰り返し処理用に1行分のDtoを用意して、2Way SQLで処理するようにします。繰り返しで更新処理が必要なケースは個別で検討します。


これで疑問が晴れて、早く寝れそうですか?(笑)