7.3.2. ユニバーサルDAO

ユニバーサルDAOでは、 JPA2.0(JSR317) (外部サイト、英語) のアノテーションを使った簡易的なO/Rマッパーを提供する。

ユニバーサルDAOの内部では、 データベースアクセス(JDBCラッパー) を使用しているので、 ユニバーサルDAOを使用するには データベースアクセス(JDBCラッパー) の設定が必要となる。

補足

ユニバーサルDAOは、簡易的なO/Rマッパーと位置付けていて、 すべてのデータベースアクセスをユニバーサルDAOで実現しようとは考えていない。 ユニバーサルDAOで実現できない場合は、素直に データベースアクセス(JDBCラッパー) を使うこと。

例えば、ユニバーサルDAOでは、主キー以外の条件を指定した更新/削除は行えないので、 データベースアクセス(JDBCラッパー) を使用する必要がある。

補足

ユニバーサルDAOは、共通項目(全てのテーブルに定義する登録ユーザや更新ユーザ等)に対する値の自動設定機能は提供しない。 共通項目に対する値の自動設定を行いたい場合は、 Domaアダプタ を適用し、Domaのエンティティリスナー機能を使用すれば良い。

どうしてもユニバーサルDAOを使用したい場合は、ユニバーサルDAOの機能を使用する前にアプリケーションで明示的に共通項目を設定すること。

7.3.2.1. 機能概要

7.3.2.1.1. SQLを書かなくても単純なCRUDができる

JPAアノテーションをEntityに付けるだけで、SQLを書かなくても、以下の単純なCRUDができる。 SQL文は、JPAアノテーションを元に実行時に構築する。

  • 登録/一括登録
  • 主キーを指定した更新/一括更新
  • 主キーを指定した削除/一括削除
  • 主キーを指定した検索

Entityに使用できるJPAアノテーションについては、 Entityに使用できるJPAアノテーション を参照。

補足

ユニバーサルDAOの上記CRUD機能では、@Tableアノテーションを使ってスキーマを指定することができる( Entityに使用できるJPAアノテーション を参照)。 ただし、 データベースアクセス(JDBCラッパー)SQL文中のスキーマを環境毎に切り替える 機能は、ユニバーサルDAOの上記CRUD機能では使用できない。環境毎にスキーマを切り替える用途には、ユニバーサルDAOではなく データベースアクセス(JDBCラッパー) を使用すること。

7.3.2.1.2. 検索結果をBeanにマッピングできる

検索では、 データベースアクセス(JDBCラッパー) と同様に、SQLファイルを作成し、SQL IDを指定した検索ができる。 さらに、ユニバーサルDAOでは、検索結果をBean(Entity、Form、DTO)にマッピングして取得できる。 Beanのプロパティ名とSELECT句の名前が一致する項目をマッピングする。

Beanに使用できるデータタイプについては、 Beanに使用できるデータタイプ を参照。

7.3.2.2. モジュール一覧

<dependency>
  <groupId>com.nablarch.framework</groupId>
  <artifactId>nablarch-common-dao</artifactId>
</dependency>

7.3.2.3. 使用方法

重要

ユニバーサルDAOの基本的な使い方は、 nablarch.common.dao.UniversalDao を参照。

7.3.2.3.1. ユニバーサルDAOを使うための設定を行う

ユニバーサルDAOを使うためには、 データベースアクセス(JDBCラッパー) の設定に加えて、 BasicDaoContextFactory の設定をコンポーネント定義に追加する。

<!-- コンポーネント名は"daoContextFactory"で設定する。 -->
<component name="daoContextFactory" class="nablarch.common.dao.BasicDaoContextFactory" />

7.3.2.3.2. 任意のSQL(SQLファイル)で検索する

任意のSQLで検索したい場合は、データベースアクセスの SQLをファイルで管理する と同様に、 SQLファイルを作成し、SQL IDを指定して検索する。

UniversalDao.findAllBySqlFile(User.class, "FIND_BY_NAME");

SQLファイルは、検索結果をマッピングするBeanから導出する。 上の例のUser.classがsample.entity.Userの場合、 SQLファイルのパスは、クラスパス配下のsample/entity/User.sqlとなる。

SQL IDに「#」が含まれると、「SQLファイルのパス#SQL ID」と解釈する。 下の例では、SQLファイルのパスがクラスパス配下のsample/entity/Member.sql、 SQL IDがFIND_BY_NAMEとなる。

UniversalDao.findAllBySqlFile(GoldUser.class, "sample.entity.Member#FIND_BY_NAME");

補足

「#」を含めた指定は、機能単位(Actionハンドラ単位)にSQLを集約したい場合などに使用できる。 ただし、指定が煩雑になるデメリットがあるため、基本は「#」を付けない指定を使用すること。

7.3.2.3.3. テーブルをJOINした検索結果を取得する

一覧検索などで、複数のテーブルをJOINした結果を取得したい場合がある。 このような場合は、非効率なため、JOIN対象のデータを個別に検索せずに、 1回で検索できるSQLJOINした結果をマッピングするBean を作成すること。

7.3.2.3.4. 検索結果を遅延ロードする

大量の検索結果を扱う処理では、メモリが足らなくなるため、検索結果をすべてメモリに展開できない。 以下のようなケースがある。

  • ウェブで大量データをダウンロードする
  • バッチで大量データを処理する

そのような場合は、ユニバーサルDAOの遅延ロードを使用する。 遅延ロードを使用すると、ユニバーサルDAOとしては1件ずつロードするが、 JDBCのフェッチサイズによってメモリの使用量が変わる。 フェッチサイズの詳細は、データベースベンダー提供のマニュアルを参照。

遅延ロードは、検索時に、 UniversalDao#defer メソッドを先に呼び出すだけで使用できる。 遅延ロードでは、内部でサーバサイドカーソルを使用しているので、 DeferredEntityList#close メソッドを呼び出す必要がある。

// try-with-resourcesを使ったclose呼び出し。
// DeferredEntityListはダウンキャストして取得する。
try (DeferredEntityList<User> users
        = (DeferredEntityList<User>) UniversalDao.defer()
                                        .findAllBySqlFile(User.class, "FIND_BY_NAME")) {
    for (User user : users) {
        // userを使った処理
    }
}

7.3.2.3.5. 条件を指定して検索する

検索画面のように、条件を指定した検索をユニバーサルDAOでも提供している。

// 検索条件を取得する
ProjectSearchForm condition = context.getRequestScopedVar("form");

// 条件を指定して検索する
List<Project> projects = UniversalDao.findAllBySqlFile(
    Project.class, "SEARCH_PROJECT", condition);

重要

検索条件は、Entityではなく検索条件を持つ専用のBeanを指定する。 ただし、1つのテーブルのみへのアクセスの場合は、Entityを指定しても良い。

7.3.2.3.6. 型を変換する

ユニバーサルDAOでは、 @Temporal を使用して、 java.util.Date 及び java.util.Calendar 型の値をデータベースにマッピングする方法を指定することができる。 他の型については、任意のマッピングは不可能であるため、Entityのプロパティは、データベースの型及び使用するJDBCドライバの仕様に応じた定義を行うこと。

また、ユニバーサルDAOは、自動生成したSQLをDBに送信する場合はJPAアノテーションの情報を使用するが、任意のSQLをDBに送信する場合はJPAアノテーションの情報は使用しない。 そのため、型変換については、以下のようになる。

Entityから自動的に生成したSQLを実行する場合
データベースへの出力時
データベースから取得時
  • @Temporal が設定されているプロパティについては、@Temporalに指定された型からの変換を行う。
  • 上記以外はEntityの情報を元に、値が変換される。
任意のSQLで検索する場合
データベースへの出力時
データベースから取得時
  • Entityから自動的に生成したSQLを実行する場合と同様の処理を行う。

重要

データベースの型とプロパティの型が不一致の場合、実行時に型変換エラーが発生する場合がある。 また、SQL実行時に暗黙的型変換が行われ、性能劣化(indexが使用されないことに起因する)となる可能性がある。

データベースとJavaのデータタイプのマッピングについては、使用するプロダクトに依存するため、 JDBCドライバのマニュアルを参照すること。

例えば、DBがdate型の場合には、多くのデータベースではプロパティの型は java.sql.Date となる。 また、DBが数値型(integerやbigint、number)などの場合は、プロパティの型は int (java.lang.Integer) や long (java.lang.Long) となる。

7.3.2.3.7. ページングを行う

ユニバーサルDAOの検索は、ページングをサポートしている。 ページングは、検索時に、 UniversalDao#per メソッド、 UniversalDao#page メソッドを先に呼び出すだけで使用できる。

EntityList<User> users = UniversalDao.per(3).page(1)
                            .findAllBySqlFile(User.class, "FIND_ALL_USERS");

ページングの画面表示に必要な検索結果件数といった情報は、 Pagination が保持している。 Pagination は、 EntityList から取得できる。

Pagination pagination = users.getPagination();

補足

ページング用の検索処理は、 データベースアクセス(JDBCラッパー)の範囲指定検索機能 を使用して行う。

7.3.2.3.8. サロゲートキーを採番する

サロゲートキーを採番する場合は、以下のアノテーションを使用する。

ユニバーサルDAOでは、 javax.persistence.GenerationType のすべてのストラテジをサポートしている。

GenerationType.AUTO

@Id
@Column(name = "USER_ID", length = 15)
@GeneratedValue(strategy = GenerationType.AUTO)
public Long getId() {
    return id;
}
  • データベース機能に設定された Dialect を元に採番方法を選択する。 優先順位は、IDENTITY→SEQUENCE→TABLEの順となる。
  • SEQUENCEが選択された場合、シーケンスオブジェクト名は”<テーブル名>_<採番するカラム名>”となる。
  • シーケンスオブジェクト名を指定したい場合は、 @SequenceGenerator で指定する。
GenerationType.IDENTITY

@Id
@Column(name = "USER_ID", length = 15)
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Long getId() {
    return id;
}
GenerationType.SEQUENCE

@Id
@Column(name = "USER_ID", length = 15)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "seq")
@SequenceGenerator(name = "seq", sequenceName = "USER_ID_SEQ")
public Long getId() {
    return id;
}
  • シーケンスオブジェクトの名前は @SequenceGenerator で指定する。
  • sequenceName属性を省略した場合、”<テーブル名>_<採番するカラム名>”となる。
GenerationType.TABLE

@Id
@Column(name = "USER_ID", length = 15)
@GeneratedValue(strategy = GenerationType.TABLE, generator = "table")
@TableGenerator(name = "table", pkColumnValue = "USER_ID")
public Long getId() {
    return id;
}
  • レコードを識別する値は @TableGenerator で指定する。
  • pkColumnValue属性を省略した場合、”<テーブル名>_<採番するカラム名>”となる。

補足

シーケンス及びテーブルを使用したサロゲートキーの採番処理は、 サロゲートキーの採番 を使用して行う。 設定値(テーブルを使用した場合のテーブル名やカラム名の設定など)は、リンク先を参照すること。

7.3.2.3.9. バッチ実行(一括登録、更新、削除)を行う

ユニバーサルDAOでは、大量データの一括登録や更新、削除時にバッチ実行ができる。 バッチ実行を行うことで、アプリケーション稼働サーバとデータベースサーバとのラウンドトリップ回数を削減でき、パフォーマンスの向上が期待できる。

バッチ実行は以下のメソッドを使用する。

重要

batchUpdate を使用した、一括更新処理では排他制御処理を行わない。 もし、更新対象のEntityとデータベースのバージョンが不一致だった場合、そのレコードの更新は行われずに処理が正常終了する。

排他制御が必要となる更新処理では、一括更新ではなく1レコード毎の更新処理を呼び出すこと。

7.3.2.3.10. 楽観的ロックを行う

ユニバーサルDAOでは、 @Version が付いているEntityを更新した場合、自動で楽観的ロックを行う。 楽観的ロックで排他エラーが発生した場合は、 javax.persistence.OptimisticLockException を送出する。

重要

@Version は数値型のプロパティのみに指定できる。 文字列型のプロパティだと正しく動作しない。

排他エラー時の画面遷移は、 OnError を使用して行う。

// type属性に対象とする例外、path属性に遷移先のパスを指定する。
@OnError(type = OptimisticLockException.class,
         path = "/WEB-INF/view/common/errorPages/userError.jsp")
public HttpResponse update(HttpRequest request, ExecutionContext context) {

    UniversalDao.update(user); // 前後の処理は省略。

}

重要

バッチ実行(一括登録、更新、削除)を行う に記載があるように、 一括更新処理(batchUpdate)では楽観的ロックは使用できないので注意すること。

7.3.2.3.11. 悲観的ロックを行う

ユニバーサルDAOでは、悲観的ロックの機能を特に提供していない。

悲観的ロックは、データベースの行ロック(select for update)を使用することで行う。 行ロック(select for update)を記載したSQLは、 UniversalDao#findBySqlFile メソッドを使って実行する。

7.3.2.3.12. 排他制御の考え方

排他制御に使用するバージョンカラムをどのテーブルに定義するかは業務的な観点により決める必要がある。

バージョン番号を持つテーブルは、排他制御を行う単位ごとに定義し、競合が許容される最大の単位で定義する。 たとえば、「ユーザ」という大きな単位でロックすることが業務的に許容されるならば、ユーザテーブルにバージョン番号を定義する。 ただし、単位を大きくすると、競合する可能性が高くなり、更新失敗(楽観的ロックの場合)や処理遅延(悲観的ロックの場合)を招く点に注意すること。

7.3.2.3.13. データサイズの大きいバイナリデータを登録(更新)する

OracleのBLOBのように、データサイズの大きいバイナリデータを登録(更新)したい場合がある。 ユニバーサルDAOだと、データをすべてメモリに展開しないと登録(更新)できないため、 データベースが提供する機能を使ってファイルなどから直接登録(更新)すること。

詳細は、 バイナリ型のカラムにアクセスする を参照。

7.3.2.3.14. データサイズの大きいテキストデータを登録(更新)する

OracleのCLOBのように、データサイズの大きいテキストデータを登録(更新)したい場合がある。 ユニバーサルDAOだと、データをすべてメモリに展開しないと登録(更新)できないため、 データベースが提供する機能を使ってファイルなどから直接登録(更新)すること。

詳細は、 桁数の大きい文字列型のカラム(例えばCLOB)にアクセスする を参照。

7.3.2.3.15. 現在のトランザクションとは異なるトランザクションで実行する

データベースアクセス(JDBCラッパー)現在のトランザクションとは異なるトランザクションでSQLを実行する と同じことを、ユニバーサルDAOで行う方法を説明する。

個別トランザクションを使用するには、以下の手順が必要となる。

  1. コンポーネント設定ファイルに SimpleDbTransactionManager を定義する。
  2. SimpleDbTransactionManager を使用して、新たなトランザクションでユニバーサルDAOを実行する。

以下に使用例を示す。

コンポーネント設定ファイル

コンポーネント設定ファイルに SimpleDbTransactionManager を定義する。

<component name="find-persons-transaction"
    class="nablarch.core.db.transaction.SimpleDbTransactionManager">

  <!-- connectionFactoryプロパティにConnectionFactory実装クラスを設定する -->
  <property name="connectionFactory" ref="connectionFactory" />

  <!-- transactionFactoryプロパティにTransactionFactory実装クラスを設定する -->
  <property name="transactionFactory" ref="transactionFactory" />

  <!-- トランザクションを識別するための名前を設定する -->
  <property name="dbTransactionName" value="update-login-failed-count-transaction" />

</component>
実装例

コンポーネント設定ファイルに設定した SimpleDbTransactionManager を使って、ユニバーサルDAOを実行する。 なお、 SimpleDbTransactionManager を直接使うのではなくトランザクション制御を行う、 UniversalDao.Transaction を使用すること。

まず、 UniversalDao.Transaction を継承したクラスを作成する。

private static final class FindPersonsTransaction extends UniversalDao.Transaction {

    // 結果を受け取る入れ物を用意する。
    private EntityList<Person> persons;

    FindPersonsTransaction() {
        // SimpleDbTransactionManagerをsuper()に指定する。
        // コンポーネント定義で指定した名前、またはSimpleDbTransactionManagerオブジェクトを指定できる。
        // この例では、コンポーネント定義で指定した名前を指定している。
        super("find-persons-transaction");
    }

    // このメソッドが自動的に別のトランザクションで実行される。
    // 正常に処理が終了した場合はトランザクションがコミットされ、
    // 例外やエラーが送出された場合には、トランザクションがロールバックされる。
    @Override
    protected void execute() {
        // executeメソッドにUniversalDaoを使った処理を実装する。
        persons = UniversalDao.findAllBySqlFile(Person.class, "FIND_PERSONS");
    }

    // 結果を返すgetterを用意する。
    public EntityList<Person> getPersons() {
        return persons;
    }
}

そして、 UniversalDao.Transaction を継承したクラスを呼び出す。

// 生成すると別のトランザクションで実行される。
FindPersonsTransaction findPersonsTransaction = new FindPersonsTransaction();

// 結果を取得する。
EntityList<Person> persons = findPersonsTransaction.getPersons();

7.3.2.4. 拡張例

7.3.2.4.1. DatabaseMetaDataから情報を取得できない場合に対応する

データベースによっては、シノニムを使用している場合や権限の問題で、 java.sql.DatabaseMetaData から主キー情報を取得できない場合がある。 主キー情報を取得できなくなると、主キーを指定した検索が正しく動作しない。 そのような場合は、 DatabaseMetaDataExtractor を継承したクラスを作成して対応する。 主キー情報をどのように取得するかはデータベース依存のため、製品のマニュアルを参照すること。

作成したクラスを使用するには、設定が必要となる。

<!--
sample.dao.CustomDatabaseMetaDataExtractorを作成した場合の設定例
コンポーネント名は"databaseMetaDataExtractor"で設定する。
-->
<component name="databaseMetaDataExtractor" class="sample.dao.CustomDatabaseMetaDataExtractor" />

7.3.2.5. Entityに使用できるJPAアノテーション

Entityに使用できるJPAアノテーションは以下のとおり。

重要

ここに記載のないアノテーション及び属性を使用しても機能しない。

フィールドに設定する場合には@Accessで明示的に指定すること。@Accessで明示的に指定した場合のみ、フィールドのアノテーションを参照する。

フィールドにアノテーションを設定する場合でも、UniversalDaoでは値の取得と設定はプロパティを通して行われるため、getterとsetterは必ず作成すること。

フィールドとプロパティは名前で紐づいているため、名前が異なるとフィールドのアノテーションをプロパティで参照できなくなる。 そのためフィールド名と、プロパティ名(get〇〇,set〇〇の〇〇の部分)を必ず同じものにすること。

補足

例えば、Lombokのようなボイラープレートコードを生成するライブラリを使用する場合、 アノテーションをフィールドに設定することでgetterを自分で作成する必要がなくなり、 ライブラリの利点をより活かすことができる。

javax.persistence.Entity

データベースのテーブルに対応したEntityクラスに設定するアノテーション。

本アノテーションを設定した場合、クラス名からテーブル名が導出される。 クラス名(パスカルケース)をスネークケース(全て大文字)へ変換した値がテーブル名となる。

Bookクラス        -> BOOK
BookAuthorクラス  -> BOOK_AUTHOR

補足

クラス名からテーブル名を導出できない場合は、 後述の @Table を用いて明示的にテーブル名を指定すること。

javax.persistence.Table

テーブル名を指定するために使用するアノテーション。

name属性に値が指定されている場合、その値がテーブル名として使用される。 schema属性に値が指定されている場合、指定されたスキーマ名を修飾子として指定してテーブルにアクセスを行う。 例えば、schema属性にworkと指定した場合で、テーブル名がusers_workの場合、work.users_workにアクセスを行う。

javax.persistence.Access

アノテーションを設定する場所を指定するために使用するアノテーション。

明示的にフィールドに指定した場合のみ、フィールドのアノテーションを参照する。

javax.persistence.Column

カラム名を指定するために使用するアノテーション。

name属性に値が指定されている場合、その値がカラム名として使用される。

補足

本アノテーションが設定されていない場合は、プロパティ名からカラム名が導出される。 導出方法は、テーブル名の導出方法と同じである。 詳細は、 @Entity を参照。

javax.persistence.Id

主キーに設定するアノテーション。

複合主キーの場合には、複数のgettterもしくはフィールドに本アノテーションを設定する。

javax.persistence.Version

排他制御で使用するバージョンカラムに設定するアノテーション。

本アノテーションは数値型のプロパティのみに指定できる。 文字列型のプロパティだと正しく動作しない。

本アノテーションが設定されている場合、 更新処理時にバージョンカラムが条件に自動的に追加され楽観ロックが行われる。

補足

本アノテーションは、Entity内に1つだけ指定可能。

javax.persistence.Temporal

java.util.Date 及び java.util.Calendar 型の値を データベースにマッピングする方法を指定するアノテーション。

value属性に指定されたデータベース型に、Javaオブジェクトの値を変換してデータベースに登録する。

javax.persistence.GeneratedValue

自動採番された値を登録することを示すアノテーション。

strategy属性に採番方法を設定する。 AUTOを設定した場合、以下のルールにて採番方法が選択される。

  • generator属性に対応するGenerator設定がある場合、そのGeneratorを使用して採番処理を行う。
  • generatorが未設定な場合や、対応するGenerator設定がない場合は、 データベース機能に設定された Dialect を元に採番方法を選択する。 優先順位は、IDENTITY→SEQUENCE→TABLEの順となる。

generator属性に任意の名前を設定する。

補足

@GeneratedValue を使用して、 シーケンス採番のシーケンスオブジェクト名や テーブル採番のレコードを識別する値を取得できない場合は、 それぞれの値をテーブル名と自動採番するカラム名から導出する。

テーブル名「USER」、採番するカラム名「ID」  -> USER_ID
javax.persistence.SequenceGenerator

シーケンス採番を使用する場合に設定するアノテーション。

name属性には、@GeneratedValue のgenerator属性と同じ値を設定する。

sequenceName属性には、データベース上に作成されているシーケンスオブジェクト名を設定する。

補足

シーケンス採番は、採番機能を使用して行う。 このため、 採番用の設定 を別途行う必要がある。

javax.persistence.TableGenerator

テーブル採番を使用する場合に設定するアノテーション。

name属性には、 @GeneratedValue のgenerator属性と同じ値を設定する。

pkColumnValue属性には、採番テーブルのレコードを識別するための値を設定する。

補足

テーブル採番は、採番機能を使用して行う。 このため、 採番用の設定 を別途行う必要がある。

7.3.2.6. Beanに使用できるデータタイプ

検索結果をマッピングするBeanに使用できるデータタイプは以下のとおり。

重要

ここに記載のないデータタイプに対して、検索結果をマッピングできない(実行時例外となる)。

java.lang.String
java.lang.Short
プリミティブ型も指定可能。プリミティブ型の場合、 null0 として扱う。
java.lang.Integer
プリミティブ型も指定可能。プリミティブ型の場合、 null0 として扱う。
java.lang.Long
プリミティブ型も指定可能。プリミティブ型の場合、 null0 として扱う。
java.math.BigDecimal
java.lang.Boolean
プリミティブ型も指定可能。プリミティブ型の場合、 nullfalse として扱う。 ラッパー型(Boolean)の場合は、リードメソッド名はgetから開始される必要がある。 プリミティブ型の場合は、リードメソッド名がisで開始されていても良い。
java.util.Date
JPAの @Temporal でデータベース上のデータ型を指定する必要がある。
java.sql.Date
java.sql.Timestamp
byte[]

BLOBなどのように非常に大きいサイズのデータ型の値は、 本機能を用いてデータをヒープ上に展開しないように注意すること。 非常に大きいサイズのバイナリデータを扱う場合には、 データベースアクセスを直接使用し、Stream経由でデータを参照すること。

詳細は バイナリ型のカラムにアクセスする を参照。