ConditionBeanの使い方

ConditionBeanの実装手順

目的ドリブンで

目的ドリブンの思考手順 を頭の中に入れた上で...

  1. 基点テーブル何か?
  2. 取得したい関連テーブルは何か?
  3. どんな絞り込み、並び替えをしたいか?
  4. 一件検索なのか?リスト検索なのか?

これがそのまま実装手順となります。

e.g. 基点テーブルは "会員" @Java

// 1. 基点テーブルのConditionBeanを生成 (基点テーブル何か?)
//  {select句, from句}
MemberCB cb = new MemberCB();

// 2. 取得したい関連テーブルを指定 (取得したい関連テーブルは何か?)
//  {select句, join句}
cb.setupSelect_MemberStatus();

// 3. 絞り込み条件・並び替え条件を設定 (どんな絞り込み、並び替えをしたいか?)
//  {where句, order-by句, (from句, on句)}
cb.query().setMemberName_PrefixSearch("S");
cb.query().addOrderBy_Birthdate_Desc();
cb.query().addOrderBy_MemberId_Asc();

// 4. Behaviorのメソッドを呼ぶ (一件検索なのか?リスト検索なのか?)
//  {一件検索?リスト検索?ページング検索?}
List<Member> memberList = memberBhv.selectList(cb);

// あとは、検索結果をお好きなように
for (Member member : memberList) {
    String memberName = member.getMemberName();
    Date birthdate = member.getBirthdate();
    String statusName = member.getMemberStatus().getMemberStatusName();
    ...
}

すると、以下のようなSQLが実行されます。

e.g. リスト検索のテスト実装のログ {select句を一部省略} @Log
(XLog#log():38) - /===========================================================================
(XLog#log():38) -                                                       MemberBhv.selectList()
(XLog#log():38) -                                                       =====================/
(XLog#log():38) - BehaviorBasicTest.test_selectList_Tx():118 -> ...
(QLog#log():38) - 
select dfloc.MEMBER_ID as MEMBER, dfloc.MEMBER_NAME ...
     , dfrel_0.MEMBER_STATUS_CODE as MEMBER_STATUS_CODE_0, ...
  from MEMBER dfloc 
    left outer join MEMBER_STATUS dfrel_0 on dfloc.MEMBER_STATUS_CODE = dfrel_0.MEMBER_STATUS_CODE 
 where dfloc.MEMBER_NAME like 'S%' escape '|' 
 order by dfloc.BIRTHDATE desc, dfloc.MEMBER_ID asc
(XLog#log():38) - ===========/ [00m00s064ms (4) first={12, Suker, Suker, PRV, null, 1968-01-01, 2010-03-10 17:42:34.402, foo, bar, 2010-03-10 17:42:34.404, foo, bar, 0}(memberStatus)@21b]
(XLog#log():38) -  

習慣として

ConditionBeanの変数名は cb で
みんなで統一した変数名を使うことで可読性が良くなります。メソッド内に二回以上、ConditionBean を new するような場合は、そもそもメソッドを分割した方が良いでしょう。
取得、絞り込み、並び替えの順番で
SetupSelect (データの取得) や Query (データの絞り込み、並び替え) は順番が前後しても動作しますが、可読性のために目的ドリブンの思考手順の通りに書くのが良いでしょう。

コード補完を駆使して実装

コード補完を最大限活用して、スピーディーに実装するのがデフォルトです。

※ここでは Eclipse を基本として説明します。

変数の抽出

受け取りの型(左辺)を先に書くのではなく、実体(右辺)を先に書いてから受け取りの型を自動で抽出する方法があります。 実体(右辺)のセミコロンの右側にカーソルが置いてある状態で、ctrl + 2 (Macは command + 2)を押した後(0.x秒)、L を押します。

e.g. Eclipseでのショートカット (変数の抽出) @Java
new MemberCB(); // セミコロンの右側にカーソル置いて ctrl (or command) + 2 and L
 to
MemberCB cb = new MemberCB(); // 変数名は習慣的に "cb"

先頭文字で勝負を決める

先頭文字を打てば、後はコード補完が導いてくれます。 特にConditionBeanでは、先頭文字(もしくはあと一文字)を打つだけで補完できるメソッドを多く利用します。

唯一の se で始まる setupSelect...()、唯一の q で始まる query()、 これだけでも覚えておく(習得する)だけで、ConditionBeanライフは大分違ったものになるでしょう。

e.g. Eclipseでのショートカット (setupSelect) @Java
cb.se // ".se" とだけ打ってメソッド(関連テーブル)を選んで enter
 to
cb.setupSelect_MemberStatus() // "se" で始まるメソッドは setupSelect... のみ
e.g. Eclipseでのショートカット (query) @Java
cb.q // ".q" とだけ打って enter
 to
cb.query() // "q" で始まるメソッドは query() のみ

その他メソッドに関しても、先頭文字をおおよそ把握しておくとさらに良いでしょう。

基本は enter がお奨め

ちなみに、enter でなくても、"." を押すことでメソッドの選択(決定)はできます。しかも、その後のコード補完もすぐに開始できて、 手間が一つ減ります。ただし、戻り値が存在していない場合は効かず、結局 enter を押す必要があります。戻り値がある場合とない場合とで、 enter なのか "." なのかを判断するくらいなら、全て enter で統一、という方がやりやすいと考えられるため(人に寄ります)、 ドキュメント上は、enter 方式で全て書いています。(enterキーの方が大きいのでリズミカルに打ちやすいというものあり)

キャメルケースコード補完

特に キャメルケースコード補完 は、ConditionBean実装を充実させます。必ず利用しましょう。

e.g. Eclipseでのショートカット (query - set...) @Java
// query()の後 ".set" とだけ打って補完候補からメソッド選択
// set[カラム名]_[比較条件名]の形式のメソッドが大量に補完される
cb.query().set
--
// メソッドを単語の先頭の大文字部分だけを入力して補完候補を絞り込む
// setMemberName_PrefixSearch()であれば、".setMNPS" と入力し、enter
cb.query().setMNPS
--
cb.query().setMemberName_PrefixSearch() // 後は引数に値を入れてセミコロンで締める
e.g. Eclipseでのショートカット (query - addOrderBy...) @Java
// query()の後 ".add" とだけ打って補完候補からメソッド選択
// addOrderBy_[カラム名]_[Asc or Desc]の形式のメソッドが大量に補完される
cb.query().add
--
// メソッドを単語の先頭の大文字部分だけを入力して補完候補を絞り込む
// addOrderBy_MemberName_Desc()であれば、".addOBMND" と入力し、enter
cb.query().addOBMND
--
cb.query().addOrderBy_MemberName_Desc() // 後はセミコロンで締める

for文の補完

検索結果などでリストを取り扱うことが多いと思います。for文は一文字一文字書かず補完しましょう。 リストや配列の下の行にて fore まで打って、ctrl + space そして enter

e.g. キャメルケースコード補完 @Java
List<Member> memberList = memberBhv.selectList(cb);
// ここで fore -> ctrl + space -> enter
--
List<Member> memberList = memberBhv.selectList(cb);
for (Member member : memberList) {

}

fore まで打って補完で、foreach の補完が一番上に候補として上がるので、候補を見ずに enter していい。ただ、例えば ForeginKey というクラスがそのプロジェクトに存在するような場合はそちらが優先されるので、矢印キーでちょっと調整する必要がある。

その他ショートカット

他にも ConditionBean を実装する上で役に立つコード補完やショートカットがあります。最初は慣れるまでちょっと "最初からベタに書いた方が速いんじゃないの?" って感じになってしまいますが、その後長く実装を続けるのであれば非常に効果的な道具となるでしょう。

基本仕様

条件の連結
(複数の条件を指定した場合に)基本的に and で連結します。(OrScopeQuery 機能で or 連結)
nullや空文字の扱い
基本的に 条件値が null もしくは空文字の場合は設定された条件は無効(条件付与しない) になります。条件値がリストの場合で、リストの中の null や空文字も同様、有効な条件が何もない空リストも同様。 ただし、無効な条件値を例外にするオプション、および、空文字を条件値として取扱うオプションもあります。
同じカラムで同じ比較条件の複数指定
基本的に 上書き ですが、比較条件次第で追加条件となることもあります。
同じカラムで同じ条件値での複数指定
基本的に 重複した条件にはならずにデバッグログが出力されますが、比較条件次第で追加条件となることもあります。
結合の方式
基本的に left outer join で結合します。(InnerJoin 機能で inner join 結合)
InnerJoin
メソッドの呼び出し順序
基本的に 以下のような順序でメソッドを呼び出すことを(習慣的に)お奨めします。
  • setupSelect...
  • query().set...
  • union(...)
  • query().addOrderBy...
  • paging(), lockForUpdate(), ...
基本的には順序が入れ替わっても動作はしますが、一部処理は順序が通りでないと動作しないものもあります。例えば、union を利用する場合は必ず setupSelect の後でなければなりません。可読性のためにも この順序(SQLのイメージと近い形)を習慣とするようにして下さい。
マルチスレッド対応
スレッドセーフではない ので、スレッド間で(同期なしの)再利用はしてはいけません。
ライフサイクル
基本的には、使い捨てです。DBアクセスするときにインスタンスを生成し、実行したらそのまま破棄。 スレッドセーフではないので、持ち回しての利用は基本的に想定されません。
検索結果のキャッシュ
検索結果をキャッシュするようなことはありません
Serializableではない
Serializableではありません。

Behaviorとのコラボ

Behaviorインスタンスの取得

Behaviorインスタンスは DIコンテナから取得 します。

CB関連のBehaviorのメソッド

selectEntity(cb)
一件検索。結果がない場合はnullを戻す
selectEntityWithDeletedCheck(cb)
一件検索。結果がない場合は例外発生
selectList(cb)
リスト検索
selectPage(cb)
ページング検索。cb.paging()とセットで利用
selectCursor(cb, entityRowHandler)
カーソル検索。一件ずつメモリ展開して処理
scalarSelect(cb)
スカラ検索。max(), min(), sum(), avg()
LoadReferrer
子テーブルの取得。Refererrテーブル毎にメソッドがある
queryUpdate(entity, cb)
ConditionBeanの条件で更新
queryDelete(cb)
ConditionBeanの条件で削除

区分値

区分値設定がされているカラムに関しては、区分値に関する条件設定のメソッドが存在します。 これらを利用することで、区分値条件をタイプセーフに指定することができます。

区分値の設定

set[カラム名]_[ConditionKey]_[区分値要素の名前]() の形式のメソッドを利用します。サポートされている ConditionKey は、Equal、NotEqual です。

e.g. 区分値条件の設定(メソッド解決)。会員ステータスが正式会員であること。 @Java
MemberCB cb = new MemberCB();
cb.query().setMemberStatusCode_Equal_Formalized();
// cb.query().setMemberStatusCode_Equal("FML"); と同義
通常のプロパティの設定と同じ set 始まり
通常のプロパティの設定と同じ set 始まりのメソッドなので、他のカラムと同様、とりあえず set と打って、対象のカラムの set メソッドを補完すると、区分値メソッドも一緒に補完候補に並びますので、後はそこから選ぶだけです。 その時、ネイティブ型の設定メソッドしか存在しない場合は、区分値が関連付けられていません(設定漏れ)ので、(ディベロッパーは)アーキテクトに相談しましょう。
キャメルケースコード補完の活用
キャメルケースコード補完で、スムーズにメソッドを呼び出せます。例えば、setMemberStatusCode_Equal_Formalized() メソッドであれば、setMSCEF と打ち込めばほぼピンポイントで絞り込めるでしょう。
CDef を直接渡すメソッド
メソッド名解決のメソッドに対し、ENUMである CDef を直接渡すメソッドもあります。set[カラム名]_[ConditionKey]_As[区分値の名前](CDef) という形式のメソッドです。ただ、Equal、NotEqualに関しては、メソッド名解決のメソッドがあるため(そちらの方が便利であるため)、基本的にはあまり利用する必要はありません。 複数の区分値要素を指定する InScope、NotInScope において利用します。
e.g. 区分値条件の設定(CDef指定)。会員ステータスが正式会員、仮会員であること。 @Java
MemberCB cb = new MemberCB();
List<CDef.MemberStatus> statusList = new ArrayList<CDef.MemberStatus>();
statusList.add(CDef.MemberStatus.Formalized);
statusList.add(CDef.MemberStatus.Provisional);
cb.query().setMemberStatusCode_InScope_AsMemberStatus(statusList);
区分値が関連付いたカラムのネイティヴ型での設定メソッドは残っています。 ですが、よほどのことがない限り、区分値コードをハードコードした利用は絶対にやってはいけません
e.g. (ディベロッパーが)絶対にやってはいけないやり方 @Java
cb.query.setMemberStatusCode_Equal("FML"); // 絶対にだめ
ネイティヴ型のメソッドが補完候補となる場合、一緒に区分値メソッドも候補として表示されるため、 区分値メソッドの存在に気付かずにネイティヴ型を使ってしまうケースはあまりないでしょうが、"念には念を" ということで、DBFluteプロパティのオプションで、このメソッドを削除して利用できないようにすることができます。
大なり小なり比較・曖昧検索は生成されない
区分値が関連付けられたカラムに対しては、GreaterEqual や LessThan などの大なり小なり比較をするメソッド、ならびに、PrefixSearch や LikeSearch などの曖昧検索のメソッドは生成されません。 業務的に区分値のコードに対してそのような比較条件を適用することはないと想定されるため、間違いを生まないようにとそのようにしています。
もし、区分値の要素間で順列を付けるのであれば、コード値に意味を兼任させるのではなく、ExampleDBの "会員ステータスの表示順" のように、別のカラムで明示的な業務名で表現するのが良いでしょう(コード値はあくまで純粋に識別値として扱うもの)。 もしくは、SubItemMap を使って独自の属性を追加しても良いでしょう。

再利用メソッドの定義

ConditionBeanでは再利用メソッドを定義して、業務的に定型的な検索条件、つまり where 句を再利用することができます。

e.g. ある特別な商品を購入したことのある会員名称が "S" で始まる正式会員という条件を設定するメソッド @Java
public class MemberCQ extends BsMemberCQ {

    ...

    /**
     * Arrange the query for selecting service members.
     * o starts 'S'
     * o status 'Formalized'
     * o exists the special product
     */
    public void arrangeServiceMember() {
        setMemberName_PrefixSearch("S");
        setMemberStatusCode_Equal_Formalized();
        existsPurchaseList(new SubQuery<PurchaseCB>() {
            public void query(PurchaseCB subCB) {
                subCB.query().setProductId_Equal(SPECIAL_PRODUCT_ID);
            }
        });
    }
}