データベースアクセス
業務アプリケーションにはデータベースへのアクセスが必要ですが、
そのためにSQL文の構築とその実行をコーディングすることが最も面倒な作業です。
そこで PersisterパターンとQueryオブジェクトを組み込むことにより、
SQL文の構築をフレームワークにまかます。
SQL文を書いた方が早いなんてことはない...と思います。
データベースアクセスはPersisterインタフェースを実装するクラスが行います。
1レコードを表現するエンティティオブジェクトにどのクラスを使うかにより
GenericPersister, BasicPersister, DynaBeanPersister があります。
アクションクラスに上記の型のフィールドを用意しておくと
アクション呼出し時に新規にPersisterオブジェクトを作成してそのフィールドにセットします。
データベース接続はデフォルトのコネクタがセットされますが、
別の接続を使用する場合は @Connector で注釈します。
public class AccountAction extends ServiceAction {
@Connector ("otherDB")
private BasicPersister accountPersister;
...
コンストラクタでPersisterオブジェクトを作成する場合コネクタを渡します。
Persister accountPersister = new BasicPersister(getConnector("otherDB"));
Persisterには操作対象のテーブルを特定する必要がありますが、
これは @TableDEF 注釈で指定します。
public class AccountAction extends ServiceAction {
@TableDEF ("account")
private BasicPersister accountPersister;
...
GenericPersisterではエンティティクラスに @TabelDEF 注釈をしておくと良いです。
@TableDEF("account")
public class Account implements Serializable {
...
GenericPersisterは @TabelDEF 注釈が見つからない場合、
エンティティクラスのクラス名をテーブル名と解釈します。
@TableDEF では複数のテーブルを指定することもでき、
この例のようにテーブルの別名も記述できます。
@TableDEF ("account a, accountitem b")
テーブル結合も可能です。
@TableDEF ("account a LEFT JOIN accountitem b ON a.id = b.accountid")
検索の場合は TableDEF に指定した文字列を'FROM'の後にSQLとして出力します。
Persisterは文字列を解析してテーブル名、別名、テーブル結合を格納する
Tableクラスのオブジェクトとして保持しています。
Persister#getTable(String) にテーブル名もしくはテーブル別名を渡せば
Tableオブジェクトを取得することができます。
同じテーブルを結合する場合は別名を指定しないといけませんね。
複数のテーブルを対象としてテーブル別名を指定している場合、
検索条件などでこのTableオブジェクトと使ってカラムを作成すると
それがどのテーブルのカラムなのかを明確にすることにより
テーブル名とか別名とかで問題が起こるのを防ぐことができます。
@TableDEF ("account a, accountitem b")
private BasicPersister persister;
public Object execute throws Exception {
Criteria where =
new Compare(new Column("accountid",
persister.getTable("accountitem")), "=", id);
return persister.select(where);
}
Persisterをコンストラクタで構築する場合は
Persister#addTable(String) もしくは Persister#addTable(Table)
でテーブルをセットします。
Persister accountPersister = new BasicPersister(getConnector("));
accountPersister.addTable(new Table("account", "a"));
accountPersister.addTable(new Table("accountitem", "b"));
Tableクラスの詳細はQueryObjectのドキュメントを参照してください。
Persisterが複数のテーブルを操作対象としていても
登録、更新、削除に関しては最初に登録されたテーブルだけが対象となります。
GenericPersisterのエンティティクラスのフィールドで
FieldDEF#table によりそのカラムが属するテーブルが明確になっている場合以外は
登録、更新データには1番目以降のテーブルの情報を格納しないようにしてください。
@Table ( "account INNER JOIN item_master ON account.item_code = item_master.code)
public Account implements Serializable {
@FieldDEF (table = "account", column = "item_code")
private String itemCode;
// 更新、登録の場合は無視される
@FieldDEF (table = "item_master", column = "name")
private String itemName;
}
このようにテーブルを明確にしておくと
このAccountオブジェクトは 'account' テーブルの更新にそのまま使用することができます。
Persisterの実行メソッドを呼び出すとSQL文を作成してデータベースに送られます。
SQLの実行にはPreparedStatementを使用しており、
カラム値や条件文の値は'?'としてSQL文に埋込まれて値をバインドしています。
値に埋込まれたSQL文が実行されるなんてことはありません。
但しログにはTRACEレベルで値を埋込んだSQL文を出力しますので、
期待されたSQL文が発行されているかのチェックに利用してください。
Persisterには以下のSQL実行メソッドがあります。
(Eはエンティティクラスの型 - BasicPersisterの場合は Map<String,Object> になります。)
- find(E)
キー情報から条件を構築して1件の結果を返します。
- find(Criteria)
指定した条件で検索を行い1件の結果を返します。
複数件のレコードが検索された場合、最初の1レコードを返します。
- select(SelectOption)
検索オプションを指定して検索を行います。
SelectOptionはWHERE句以外にも
並び順、グループ、DISTINCT
等を設定できます。
検索件数が WEB-INF/application-settings.xml の
select-limit で設定した件数を超える場合
検索順に select-limit で指定した件数のレコードのみが返されます。
大量のレコードが検索される可能性がある場合は次の範囲指定検索を使用してください。
- select(SelectOption, int, int)
検索範囲を指定して検索を行います。
検索された順にレコードに振られる1から始まるインデックスで範囲を指定します。
このメソッドは select-limit の制限は受けません。
例を以下に示します。
**** Java ****
public class XXXAction extends ServiceAction {
public Object execute() throws Exception {
...
// 101件を検索することにより100件を超えるレコードの存在を確認する
return persister.select(option, 1, 101);
}
}
**** JavaScript ****
doAction('XXXAction', params, function(data) {
if (data.length > 100) {
$("next").show(); // "次の100件"を表示
data.pop(); // 101件目を削除
}
// データを表示
});
- update(E)
キー情報から条件を構築して更新を行います。
- update(E, Criteria)
指定した条件で更新を行います。
- insert(E)
引数のオブジェクトの内容をテーブルに登録します。
- delete(E)
キー情報から条件を構築して削除を行います。
- delete(Criteria)
指定した条件で削除を行います。
- executeQuery(String, Object...)
指定した検索SQL文を実行します。
SQL文に'?'が含まれる場合はバインド変数として
その値として第二引数以降のオブジェクトをバインドします。
'?'が含まれない場合は指定しなくても大丈夫です。
これは下の2つのメソッドも同様です。
- executeUpdate(String, Object...)
指定した更新SQL文を実行します。
- execute(String, Object...)
指定したSQL文を実行します。
テーブルのカラムに対応するフィールドを持つエンティティクラスを使用するPersisterです。
カラム毎のバリデーションやパラメータ名とカラム名の対応などを細かく指定することができます。
エンティティクラスを作成するのは面倒ですが、
サンプルにデータベースと接続してエンティティクラスを生成するツールを用意しましたのでご利用ください。
テーブルおよカラムの情報はエンティティクラスで指定します。
アクションクラスのフィールドでは @Entity 注釈します。
public Class UpdateAccountAction extends ServiceAction {
@Entity (Account.class)
private GenericPersister<Account> accountPersister;
...
GenericPersisterをコンストラクタで構築する場合は引数にエンティティクラスを指定します。
Persister persister = new GenericPersister<Account>(getConnector(), Account.class);
総称でAccountを指定しているのだからAccountクラスを別に指定しなくとも...と思いますが、
総称はコンパイル後に削除されてしまうので仕方ありません。
総称を指定しておくと醜いキャストが不要になりますので指定しておきましょう。
エンティティクラスを設定しない場合、
エンティティオブジェクトを引数とするメソッドが呼ばれるとその時にエンティティクラスをセットしますが、
select(SelectOption) などの引数にエンティティを渡さないメソッドが先に呼ばれると
例外がスローされます。
エンティティクラスは次のように作成します。
@Table ("acount a, accountitem b")
public class Account implements Serializable {
@FieldDEF (
table = "account",
column = "id",
key = true,
validator = ByteLengthValidator,
vlidatorParams = "4"
)
priavate String accountID;
public String getAccountID() { return accountID; }
public void setAccountID(String accountID) { this.accountID = accountID; }
}
@TableDEF 注釈がない場合はクラス名をテーブル名と解釈し、
フィールドに @FieldDEF 注釈がない場合はフィールド名をカラム名と解釈します。
但しフィールドが static もしくは final である場合はカラムとは扱われません。
カラムと関連づかないフィールドは ignore = true に設定しなけれなばりません。
@FieldDEF (ignore = true)
private String localUse;
フィールドの値が null もしくは空文字列の場合は
そのフィールドは更新、登録時に変更対象カラムとはなりません。
結果としてそのカラムは更新の場合は既存の値、登録の場合は NULL となります。
NULLもしくは空文字列で更新する場合は
acceptNull、acceptBlank をセットします。
// null, 空文字列 どちらの場合も NULL で更新
@FieldDEF (acceptNull = true)
private String aaa;
// 空文字列の場合は空文字列で更新 nullの場合は更新しない
@FieldDEF (acceptBlank = true)
private String bbb;
// 空文字列の場合は空文字列で、nullの場合は NULL で更新
@FieldDEF (acceptNull = true, acceptBlank = true)
private String ccc;
フィールドがプリミティブ型にすると常に更新対象となり、
値をセットしないと常にゼロで更新されてしまいます。
これを避けるため
フィールドをラッパー型としてGetterメソッドでプリミティブ型を返すようにします。
private Integer amount;
// 値が NULL の場合は -1 を返す。
public int getAmount() {
if (this.amount == null) {
return -1;
} else {
return this.amount.intValue();
}
}
FieldDEF注釈の各項目はすべて任意です。詳細は以下の通りです。
-
table
テーブル名。TableDEFで別名を設定した場合は別名も可。
-
column
カラム名。カラム名とフィールド名が異なる場合は必須。
-
alias
カラムの別名。
-
key
このフィールドを主キーとして扱う場合はtrue
-
validator
フィールドの値妥当性チェックを行う場合はそのバリデータクラスを指定する。
-
validatorParams
validatorを指定した場合、そのパラメータ。
-
validatorMessage
validatorを指定した場合、そのメッセージ。
-
ignore
true の場合はこのフィールドをカラムとして扱わない。
-
acceptNull
true の場合はこのフィールドの値がnullの場合でも更新対象とする
-
acceptBlank
true の場合はこのフィールドの値が空文字列の場合でも更新対象とする
バリデーションに関しては
後述します。
BasicPersisterは Map<String,Object> オブジェクトを 1レコードとして扱うPersisterです。
エンティティクラスが不要なので簡単なSQL操作で使用できます。
また検索カラムを指定できるのでCOUNTやMAX等の検索が可能です。
BasicPersisterは必ず操作対象のテーブルを指定します。
public class SelectAccountAction extends ServiceAction {
@TableDEF ("account")
private BasicPersister accountPersister;
...
コンストラクタで構築する場合は
Persister accountPersister = new BasicPersister(getConnector());
accountPersister.addTable("account");
検索結果として作成されるMapオブジェクトには
カラム名をキーにカラムの値のオブジェクトを格納します。
このマップのキー名は小文字となります。
またカラム名に "." (ドット)が含まれる場合はそれを "_" に変換します。
通常は
テーブル名 + "." + カラム名
を期待するかもしれませんが、
JavaScriptではドットがオブジェクトの属性を表現するため、
これをJSONに変換してクライアントに返すと不具合が起こるためです。
不幸にしてたまたま同じキー名となったカラムに何が格納されているかは不定です。
検索カラムを指定しない場合はデータベースから対象テーブルのカラム情報を取得し
その全てを検索カラムとします。
検索カラムを指定するにはBasicPersister#addSelectColumn(Object...)を使用します。
引数は文字列もしくはColumnElementオブジェクト、またはその配列、集合を受け入れます。
例えばテーブルの総レコード数を取得する場合は以下のようになります。
BasicPersister persister = new BasicPersister(connector);
persister.addTable("table");
persister.addSelectColumn("COUNT(*) AS count");
Map record = persister.find((Criteria)null);
int count = ((Numeric)(record.get("count"))).intValue();
検索カラムを指定した場合、
検索で返されるMapオブジェクトから取得する集合の順序がカラムの追加順であることが保証されます。
GenericPersisterの更新、登録時にはカラムに登録される値の妥当性チェックを行います。
バリデーションの内容はエンティティクラスのフィールドの@FieldDEFの指定に従います。
但し必須チェックについてはその値が更新値として扱われる場合はチェックを行いません。
バリデーションの詳細については
バリデータを参照してください。
検索、更新、削除で指定する条件はQueryObjectのCriteriaオブジェクトで設定します。
詳細はQueryObjectのドキュメントを参照してもらうとして、
ここでは簡単な説明を述べます。
注意すべきは
検索に限っては、
基本的に条件となる値が null もしくは空文字列の場合は
その条件のSQLを出力しません。
String value2 = "";
Criteria criteria = new And(
new Compare("column1", "=", value1),
new Compare("column2", "=", value2));
persister.find(criteria);
=> "SELECT xxx, xxx FROM xxx WHERE column1 = 'value1'"
value2が空文字列のため "column2 = ''" という条件は出力されません。
空文字列を条件とするためにはこのようにします。
String value2 = "";
Value column2Value = new Value(value2);
column2Value.setBlankable(true);
Criteria criteria = new And(
new Compare("column1", "=", value1),
new Compare("column2", "=", column2Value));
persister.find(criteria);
=> "SELECT xxx, xxx FROM xxx WHERE column1 = 'value1' AND column2 = ''"
ヌルを条件とする場合は isNull を使用してください。
Criteria criteria = new And(
new Compare("column1", "=", value1),
new isNull("column2"));
persister.find(criteria);
=> "SELECT xxx, xxx FROM xxx WHERE column1 = 'value1' AND column2 IS NULL"
更新および削除については予期されない変更を防ぐために自動的に条件をスキップすることはありません。
条件が空となって全件を対象となる場合はSQLは実行されません。
結果として、更新・削除の場合は更新件数として 0 を返し、
検索は select は空のリスト、find はnullを返します。
検索については条件を設定していなければ
意図されたものとして全件検索を行います。
条件を設定していて
条件が空となった場合に全件検索を行いたい場合は
SelectOption#setAllowSelectAll(boolean)
に true を渡す事により可能となります。
SelectOption option = new SelectOption();
result = persister.select(option); // 全件検索を行う
option.addWhere(new Compare("clumn", "=", ""));
result = presister.select(option); // 空のリストを返す
option.setAllowSelectAll(true);
result = presister.select(option); // 全件検索を行う
更新・削除の場合、業務の中で全件更新したり削除することはほとんどないと思いますが、
テストなどで必要な場合は
Persister#executeUpdate(Sting, Object...)
を使ってください。
QueryObjectの条件クラスとして以下のものがあります。
-
And
複数の条件を'AND'で結合します。
コンストラクタの引数として複数の条件を渡すか、addメソッドで条件を追加します。
Orクラスのオブジェクトも追加できます。
Criteria and = new And(
new Compare("column1", "=", value1),
new Or(new Compare("column1", ">", value2),
new Compare("column2", ">", value3)));
=> column1 = ? AND (column1 > ? OR column2 > ?)
-
Or
複数の条件を'OR'で結合します。
-
CriteriaText
文字列をそのまま条件文として出力します。
文字列に'?'が含まれる場合、第二引数以降を順にバインドします。
Criteria criteria = new CriteriaText("column = ?", value);
=> column = ?
-
Compare
第二引数で指定した演算子で比較を行います。
第一引数はカラム名、第三引数は変数と解釈されますから、
カラム同士を比較する場合は下記のようにします。
Criteria criteria = new Compare("column1", "=", new Column("column2"));
=> column1 = column2
逆にカラム名と解釈される引数に変数をセットする場合は Value を使用します。
Criteria criteria = new Compare(new Value(value), "=", new Column("column2"));
=> ? = column2
-
Like
パターンマッチングを行います。
指定するパターンに'?'が含まれる場合は第三引数以降がパターンに埋込まれます。
Criteria criteria = new Like("column", "%?", "value");
=> column LIKE '%value'
部分一致、前方一致、後方一致のパターンを常数としてそれぞれ
Like.CONTAINS, Like.STARTS_WITH, Like.ENDS_WITH を用意してあります。
Criteria criteria = new Like("column", Like.CONTAINS, "value");
=> column LIKE '%value%'
-
Between
範囲指定です。
Criteria criteria = new Between("column", "value1", "value2");
=> column BETWEEN 'value1' AND 'value2'
-
In
IN演算子です。
String[] array = new String[] { "a", "b", "c" };
Criteria criteria = new In("column", array);
=> column IN ('a', 'b', 'c')
InのコンストラクタはListのような集合も受けつけるほかSELECT文も指定可能です。
-
isNull
列の値がNULLであるかの条件です。
Criteria criteria = new IsNull("column");
=> column IS NULL
-
Exists
レコードの存在チェックです。
Existsは引数としてSELECT文を指定します。
SELECT文は Persister.createSelectQuery() で取得できますが、
検索条件が空ですから検索条件を設定します。
SelectStatement select = accountPersister.createSelectQuery();
select.addWhere(new Compare("accountitem", "=", "002");
Criteria criteria = new Exists(select);
=> EXISTS (SELECT * FROM account WHERE accountitem = '002')
-
Not
指定した条件の否定表現を作成します。
Criteria criteria = new Not(new IsNull("column"));
=> column IS NOT NULL
条件検索の場合はWHERE句だけでなく他の条件を格納する
SelectOption
を渡して検索を行います。
SelectOptionは
DISTINCT指定、
WHERE句、
GROUP指定、
HAVING条件、
ORDER BY指定
を設定することができます。
SelectOption option = new SelectOption();
option.setDistinct(true);
option.addWhere(new Compare("id", "=", "001");
option.addOrder("id", true);
List results = persister.select(option);
=> SELECT DISTINCT xxx, xxx FROM xxx WHERE id = '001' ORDER BY id ASC
WHEREおよびHAVINGのどちらの条件も設定していなければ全件検索を行いますが、
条件を設定しているが値がnullもしくは空文字列のために全ての条件が空となる場合は
検索は実行されず、
selectの場合は空のリスト、findの場合はnullが返されます。
このようなケースで全件検索を行うには検索オプションの setAllowSelectAll(true) と指定する必要があります。
SelectOption option = new SelectOption();
option.addWhere(xxx);
option.setAllowSelectAll(true);
List results = presister.select(option);
また集合演算子により複数のSELECT文を組み合わせることができます。
SelectStatement select1 = persister.createSelectQuery();
SelectStatement select2 = persister.createSelectQuery();
select1.addWhere(xxx);
select2.addWhere(xxx);
SelectQuery select = new Union("UNION ALL", select1, select2);
List results = persister.executeQuery(select);
各レコードは executeQuery を実行した Persister の扱う型のオブジェクトに格納しますから、
異なるテーブルを組み合わせる場合は注意してください。
Persister.createSelectQuery()で作成されるSELECT文は
BasicPersisterでテーブルとして指定できるほか、
In, Existsの条件で値としてすることができます。