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回で検索できるSQL と JOINした結果をマッピングする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に指定された型への変換を行う。
- 上記以外については、データベースアクセス(JDBCラッパー) に処理を委譲して変換を行う。
- データベースから取得時
- @Temporal が設定されているプロパティについては、@Temporalに指定された型からの変換を行う。
- 上記以外はEntityの情報を元に、値が変換される。
- 任意のSQLで検索する場合
- データベースへの出力時
- データベースアクセス(JDBCラッパー) に処理を委譲して変換を行う。
- データベースから取得時
- 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で行う方法を説明する。
個別トランザクションを使用するには、以下の手順が必要となる。
- コンポーネント設定ファイルに SimpleDbTransactionManager を定義する。
- SimpleDbTransactionManager を使用して、新たなトランザクションでユニバーサルDAOを実行する。
以下に使用例を示す。
- コンポーネント設定ファイル
コンポーネント設定ファイルに SimpleDbTransactionManager を定義する。
- connectionFactory プロパティに ConnectionFactory 実装クラスを設定する。 ConnectionFactory 実装クラスの詳細は、 データベースに対する接続設定を行う を参照。
- transactionFactory プロパティに TransactionFactory 実装クラスを設定する。 TransactionFactory 実装クラスの詳細は、 データベースに対するトランザクション制御を行う を参照。
<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アノテーションは以下のとおり。
- クラスに設定するアノテーション
- getterまたはフィールドに設定するアノテーション
重要
ここに記載のないアノテーション及び属性を使用しても機能しない。
フィールドに設定する場合には@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
- プリミティブ型も指定可能。プリミティブ型の場合、
null
は0
として扱う。 - java.lang.Integer
- プリミティブ型も指定可能。プリミティブ型の場合、
null
は0
として扱う。 - java.lang.Long
- プリミティブ型も指定可能。プリミティブ型の場合、
null
は0
として扱う。 - java.math.BigDecimal
- java.lang.Boolean
- プリミティブ型も指定可能。プリミティブ型の場合、
null
はfalse
として扱う。 ラッパー型(Boolean)の場合は、リードメソッド名はgetから開始される必要がある。 プリミティブ型の場合は、リードメソッド名がisで開始されていても良い。 - java.util.Date
- JPAの @Temporal でデータベース上のデータ型を指定する必要がある。
- java.sql.Date
- java.sql.Timestamp
- byte[]
BLOBなどのように非常に大きいサイズのデータ型の値は、 本機能を用いてデータをヒープ上に展開しないように注意すること。 非常に大きいサイズのバイナリデータを扱う場合には、 データベースアクセスを直接使用し、Stream経由でデータを参照すること。
詳細は バイナリ型のカラムにアクセスする を参照。