JAXSON 業務アプリケーションフレームワーク
トップ

インストール

ドキュメント
チュートリアル
開発ガイド
トランザクション管理
データベース接続
データベースアクセス
バリデーション
セキュリティ
エクセルファイル出力
JavaScriptリファレンス
API doc

ダウンロード
jaxson-2.1.zip

データベースアクセス

業務アプリケーションにはデータベースへのアクセスが必要ですが、 そのためにSQL文の構築とその実行をコーディングすることが最も面倒な作業です。 そこで PersisterパターンとQueryオブジェクトを組み込むことにより、 SQL文の構築をフレームワークにまかます。 SQL文を書いた方が早いなんてことはない...と思います。

Persester
データベースアクセスは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' テーブルの更新にそのまま使用することができます。


SQL文の実行
Persisterの実行メソッドを呼び出すとSQL文を作成してデータベースに送られます。
SQLの実行にはPreparedStatementを使用しており、 カラム値や条件文の値は'?'としてSQL文に埋込まれて値をバインドしています。
値に埋込まれたSQL文が実行されるなんてことはありません。
但しログにはTRACEレベルで値を埋込んだSQL文を出力しますので、 期待されたSQL文が発行されているかのチェックに利用してください。

Persisterには以下のSQL実行メソッドがあります。
(Eはエンティティクラスの型 - BasicPersisterの場合は Map<String,Object> になります。)


GenericPersister
テーブルのカラムに対応するフィールドを持つエンティティクラスを使用する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注釈の各項目はすべて任意です。詳細は以下の通りです。 バリデーションに関しては後述します。


BasicPersister
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の指定に従います。
但し必須チェックについてはその値が更新値として扱われる場合はチェックを行いません。
バリデーションの詳細についてはバリデータを参照してください。


WHERE条件
検索、更新、削除で指定する条件は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の条件クラスとして以下のものがあります。


検索オプション
条件検索の場合は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の条件で値としてすることができます。