データアクセス層

データアクセス層は、ドメインロジックにおけるデータアクセスロジックの API とその実装を提供します。データベースの 読み書きとドメインオブジェクトへのマッピングが主な役割です。データアクセス層はドメイン層の一部であり、ドメイン層 以外のクラスが直接データアクセス層を参照することはありません。データアクセス層では、インターフェースとその実装 を DAO パターン によって分離しています。インターフェースは org.unitedfront2.dao パッケージに、その実装クラスはそれ以下のパッケージに収録されています。現在 United Front 2 で対応してい るのは MySQL に対する Spring JDBC 実装です。

Note: データアクセス層とドメイン層

ドメイン層とデータアクセス層はインターフェースを通して完全に分離されており、かつパッケージとしても分離しています が、両者は密接に結びついています。実際のところ、データアクセス層とドメイン層のパッケージは循環依存関係にあり ます。厳密にはデータアクセス層とドメイン層のクラスは同一パッケージに配置されるべきですが、それではそのパッ ケージの債務が増えてしまうというデメリットがあるため、ここではディレクトリを分ける程度の意味合いでパッケージを 使っています。本質的には、データアクセス層はドメイン層の一部分と考えてください。

データアクセスインターフェース

データアクセスインターフェースは、データへアクセスするために必要となる API を定義しています。基本的に、データ アクセスインターフェースはデータベーステーブルと1対1に対応しています。データアクセスインターフェースには回復可 能な例外(チェックされる例外)は定義されません。データの整合性違反などを検知した際に発生するのは実行時例外 になります。

データアクセス API はドメイン層のクラスが持っているようなドメイン(問題対象領域)を意識した高レベル API では なく、データを読み書きすることに意識を置いた低レベルな API です。SQL のように扱える API が適切です。 登録( register* )、取得( find* )、更新( update* )、 削除( delete* )以外のメソッド名は、粒度の点で問題がないか十分検討する必要があります。 オブジェクトのライフサイクルを次に示します。

オブジェクトのライフサイクル
Note: DAO パターンでのインターフェース仕様とテスト

DAO パターンの目的は、ビジネスロジックとデータアクセス実装をインターフェースによって明確に分離することです。こ れを達成するためには、DAO インターフェースの仕様を正しく記述し、その単体テストを正しく実装する必要があります。 正しい仕様記述と単体テストは、データアクセス実装に関係なく再利用できます。例えば、データアクセス実装がリレー ショナルデータベースではなくファイルシステムに変更されてもそれらは利用できるはずです。有名なデータアクセステ ストフレームワークとして DbUnit が挙げられますが、その名 前が示すとおりデータアクセス実装に依存した単体テストになってしまうため、ここでの採用は適切ではありません。こ こでいう仕様記述と単体テストはもっと抽象度の高いものである必要があります。United Front 2 における具体 的な仕様記述と単体テスト方式は、契約による設計 の項で述べています。

Note: 例外処理

データアクセス層から回復可能な例外処理を排除することで、データアクセス層のコードはかなりシンプルになります。 これは、肥大化しがちなデータアクセス層を薄くするための工夫の一つです。代わりに整合性を確認するための API を提供することで、ドメイン層にデータの整合性と例外処理の責任を委譲しています。データアクセス層の API は、 データの整合性が違反しないように呼び出されることを前提としています。

主要なメソッド

一般的なデータアクセスインターフェースは次のようなメソッドを持ちます。

メソッド名 説明
register ドメインオブジェクトを登録します。ドメインオブジェクトの永続プロパティがデータベースと同期します。主キーなど、データ ベース側で自動生成される値は、引数のドメインオブジェクトにその値が設定されます。このメソッドを実装するクラスは インターフェース Registerable を実装します。
find 主キーの値を引数として渡し、ドメインオブジェクトを取得します。ドメインオブジェクトの永続プロパティはデータベースと 同期しています。もし対応するドメインオブジェクトが見つからなかった場合、null を返します。この メソッドを実装するクラスの主キーが一つの整数型である場合、インターフェース SimpleFindable を実装します。
update ドメインオブジェクトを更新します。ドメインオブジェクトの永続プロパティがデータベースと同期します。データベース側で 自動生成される値は、引数のドメインオブジェクトにその値が設定されます。このメソッドを実装するクラスはインター フェース Updatable を実装します。
delete 指定した主キーを持つデータを削除します。削除対象となるドメインオブジェクトのプロパティとなるドメインオブジェクトも 極力 削除されます。プロパティとなるドメインオブジェクトが他のドメインオブジェクトからも参 照されている場合などは削除されない可能性があります。このメソッドを実装するクラスの主キーが一つの整数型であ る場合、インターフェース SimpleDeletable を実装します。

上記の基本メソッドに加え、次のようなメソッドを持つことがあります。基本的に ColumnName はドメインモ デルのプロパティに対応します。

メソッド名 説明
findColumnName 主キーの値を引数として渡し、ドメインオブジェクトのプロパティ columnName の値を取得します。
findByColumnName 任意のプロパティ columnName の値を引数として渡し、その値を持った、完全なドメインオブジェクトまたは そのリストを取得します。もし対応するドメインオブジェクトが見つからなかった場合、null または 空のリストを返します。
findByColumnName Like 任意のプロパティ columnName の値の一部を引数として渡し、その値を一部に持った、完全なドメインオ ブジェクトまたはそのリストを取得します。もし対応するドメインオブジェクトが見つからなかった場合、 null または空のリストをします。
findCondition 条件 Condition を完成させるために必要なプロパティの値を引数として渡し、条件に合った完全なドメイ ンオブジェクトまたはそのリストを取得します。もし対応するドメインオブジェクトが見つからなかった場合、 null または空のリストを返します。
count 永続化されているドメインオブジェクトの総数を返します。
updateColumnName ドメインオブジェクトの一つのプロパティ columnName を更新します。
Note: 自動生成対象となるデータ

自動生成の対象となるデータは、主キーなど、最小限のものに限定します。日付やコードなどをデータベース側で自動 生成してしまうと、API の仕様が複雑化し、柔軟性が低下します。日付やコードはドメインモデルかテーブルモジュー ルで設定し、それをデータアクセスオブジェクトへ渡す方式が好ましいでしょう。日付の生成は全てのデータアクセスオブ ジェクトで利用できますし、日付データ処理専用の DateDao を使うこともできます。コードの生成は Apache Commons Lang の RandomStringUtils を利用できます。コードサンプルとしては、Mail のソースコード を参照ください。

静的構造図

全てのデータアクセスインターフェースはインターフェース Dao を継承しています。インターフェース Dao は日付処理などの、共通的な API を定義しています。

データアクセスインターフェース

サンプルコード

サンプルコード SampleDomainDao を示します。完全なコードは SampleDomainDao のソースコード を参照してください。

public interface SampleDomainDao extends Registerable<SampleDomain>,
    SimpleFindable<SampleDomain>, Updatable<SampleDomain>, SimpleDeletable,
    Dao {

    SampleDomain findByCode(String code);

    int count();
}
Note

RegisterableUpdatableSimpleFindableDeletable を実装することは一見冗長な作業のよう に思えるかもしれません。しかしこれは設計に柔軟性を与えること以上に、仕様の再利用を促進する効果があります。 JavaDoc には、上位のクラスまたはインターフェースからドキュメントを継承する仕組みがあります。仕様に追記事項 があれば、上位のメソッドをオーバーライドし、追記分の仕様のみを記述します。

JDBC 実装

JDBC 実装には Spring の JDBC 抽象フレームワークを利用しています。JDBC 実装はアノテーションベースの Bean 定義を採用しています。詳細は Spring のドキュメント Data access using JDBC を参照してください。

Note: JDBC vs O/R マッピング

JDBC と O/R マッピング製品のどちらを選ぶべきでしょうか?JDBC の利点は、データベースを操作する際に最も効 率的で標準的な SQL を利用できる点です。反面、Java オブジェクトとリレーショナルデータベースの性質の差(イン ピーダンスミスマッチ)を埋めるためのマッピングロジックを実装する必要があります。O/R マッピング製品の主要な利 点はこのマッピングロジックをフレームワークが担当する点にありますが、SQL で実現できるの全てのケースに対応し ているわけではなく、場合によっては実装がより複雑になることもあります。United Front 2 はデータ処理が複雑 になると想定されるため、軽量で扱いやすい JDBC 実装を採用しました。単純な SQL の発行とマッピングに対して は、簡単なフレームワークも用意しています。

静的構造図

DaoSupport は全ての JDBC 実装を支援する抽象クラスです。SimpleDaoSupportregisterfindupdatedelete などの基本的なメソッドに対するデフォルトの実装を提供します。

データアクセス実装

サンプルコード

サンプルコード SampleDomainDaoImpl を示します。完全なコードは SampleDomainDaoImpl のソースコード を参照してください。

// アノテーションベースの Spring Bean 定義。
@Repository(value = "sampleDomainDao")
public class SampleDomainDaoImpl extends SimpleDaoSupport<SampleDomain>
    implements SampleDomainDao {

    // SimpleDaoSupport の抽象メソッドの実装
    @Override
    protected Class<SampleDomain> getDomainClass() {
        return SampleDomain.class;
    }

    // 以下、SampleDomainDao の実装

    @Override
    public SampleDomain findByCode(String code) {
        try {
            return getSimpleJdbcTemplate().queryForObject(
                    "SELECT * FROM SampleDomain WHERE Code = ?",
                    createRowMapper(), code);
        } catch (DataAccessException e) {
            if (logger.isInfoEnabled()) {
                logger.info(e.getMessage());
            }
            return null;
        }
    }

    @Override
    public int count() {
        return getSimpleJdbcTemplate().queryForInt(
                "SELECT COUNT(*) FROM SampleDomain");
    }
}