ConditionBeanの使い方

ConditionBeanの実装手順

目的ドリブンで

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

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

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

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

// 1. 基点テーブルのBehaviorを選んで... (基点テーブル何か?)
// 2. Behaviorのメソッドを呼ぶ (一件検索なのか?リスト検索なのか?)
//  {select句, from句}
List<Member> memberList = memberBhv.selectList(cb -> {
    // 3. 取得したい関連テーブルを指定 (取得したい関連テーブルは何か?)
    //  {select句, join句}
    cb.setupSelect_MemberStatus();

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

// あとは、検索結果をお好きなように
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) - DemoTest.test_demo():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 で
みんなで統一した変数名を使うことで可読性が良くなります。
取得、絞り込み、並び替えの順番で
SetupSelect (データの取得) や Query (データの絞り込み、並び替え) は順番が前後しても動作しますが、可読性のために目的ドリブンの思考手順の通りに書くのが良いでしょう。

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

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

※ここでは Eclipse を基本として説明しますが、IntelliJでも同じものがあるはずです

戻り値の受け取り

先にメソッドを呼びましょう!(戻り値を受け取るのはあとあと)

  1. セミコロンの右側にカーソルを置いて...
  2. ctrl+2 (Macなら command+2)を押して離して...
  3. ...離してから 0.2 秒後に L (える) を押します
e.g. Eclipseでのショートカット (メソッドの場合) @Java
memberBhv.selectList(cb -> {
    cb...
}); // セミコロンの右側にカーソル置いて ctrl+2, L (Macなら command+2, L)
--
ListResultBean<Member> memberList = memberBhv.selectList(cb -> { // ぼがーん!
    cb...
}); // 変数名はいい感じに修正、候補から選んでもOK
e.g. Eclipseでのショートカット (getの場合) @Java
member.getMemberId(); // セミコロンの右側にカーソル置いて ctrl+2, L
--
Integer memberId = member.getMemberId(); // 戻り値の型を厳密に知らなくても良い

いったん指を話した後に、別のボタンを押すので難しいかもしれません。苦手な場合は、同じカーソル位置で ctrl+1 を押して "Assign statement to new local variable" を選ぶやり方もあります。

基本的に右辺から書いていって、左辺は導出します。

newでも使えます。

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

先頭文字で勝負を決める

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

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

e.g. Eclipseでのショートカット (setupSelect) @Java
cb.se // ".se" とだけ打ってメソッド (関連テーブル) を選んで enter
--
cb.setupSelect_MemberStatus() // 完璧に絞り込む必要はない (選べれば良い)
e.g. Eclipseでのショートカット (query) @Java
cb.q // ".q" とだけ打って enter
--
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_LikeSearch()であれば、".setMNLS" と入力し、enter
cb.query().setMNLS
--
cb.query().setMemberName_LikeSearch(...) // 後は引数に値を入れてセミコロンで締める
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 してOKです。ただ、例えば ForeginKey というクラスがそのプロジェクトに存在するような場合はそちらが優先されるので、矢印キーでちょっと調整する必要があります。

もし、ループに Java8 の List@forEach() を使う場合は、(DBFluteのコード補完テンプレートが入っていれば) _fo => ctrl+space でいけます。

ローカル変数の抽出

何回も同じものを get, get してたら、ローカル変数へ!

e.g. キャメルケースコード補完 @Java
log("memberName: {}", member.getMemberName());
if (member.getMemberName().startsWith("S") || member.getMemberName().length() >= 10) {
    ... = convertToDetarame(member.getMemberName());
}
// member.getMemberName() を選択して、
// ctrl+1 => enter (extract to variable) すると...
--
// ぜんぶ一気に変数に置き換わる!
String memberName = member.getMemberName(); // どーん!
log("memberName: {}", memberName);
if (memberName.startsWith("S") || memberName.length() >= 10) {
    ... = convertToDetarame(memberName);
}

パフォーマンス上の問題というよりも、コードの見た目がきっかけかもしれません。抽出前は get, get でだいぶコードが膨れ上がっていますよね。スッキリさせるためにも、積極的に抽出していきましょう。

一方で、最初から想定して変数を用意するというのはそれはそれで難度が高いものだし、 一回だけしか使わないもので、あまりメソッド名も長くないものであれば、逆に変数になっている方がめざわりかもしれません。 最初はあまり気にせず勢いでget,getして、ふとコードを見上げたときに "えいっ" とやればいいかなと思っています。

メソッド名や変数名の変更

しっくりくる名前を気軽に試そう!

e.g. キャメルケースコード補完 @Java
... = memberEntityDayo.getMemberName()
... = memberEntityDayo.getMemberAccount()
... = memberEntityDayo.getBirthdate()
... = memberEntityDayo.getFormalizedDatetime()
// memberEntityDayo を選択して、
// ctrl+1 => enter (rename in file) して、名前を member に修正すると...
--
// ぜんぶ一気に変わる!
... = member.getMemberName()
... = member.getMemberAccount()
... = member.getBirthdate()
... = member.getFormalizedDatetime()

名前は、実際にある程度書き終わってからじゃないと、適切な名前って思いつかないものですし、実際にその名前で書いてみないとしっくりくるかどうかわからないものです。 ショートカットで名前の試行錯誤をスムーズにできるようにしましょう。

IDEに提案してもらおう

ctrl+1 (command+1) って、なんか何でも使えるショートカットに思えますね。

このショートカットは、IDEがカーソル位置から判断して、プログラマーのやりたいことを推測してリファクタリング候補を挙げてくれるものです。 リファクタリング候補は、だいたいカーソル位置から規則的に列挙されますので、ショートカットキーを忘れたときの保険のようにも使えます。

というか、ショートカットキーを覚えるのが大変であれば、とにかく ctrl+1 さえ覚えておけば良いとも言えます。 実際に、受け取り変数の導出も、ctrl+1で実現できてしまいます。大抵のことはできるように設計されているので、ぜひぜひ積極的に使っていきましょう。

その他ショートカット

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

以下、Eclipseのその他のショートカットの一部です。

一行削除
ctrl (command) + D
一行移動
alt + 上下矢印
一行コピー
alt + ctrl(command) + 上下矢印
単語ごとカーソル移動
alt + 左右矢印
メソッドごとの移動
control + alt + 上下矢印 (Macの場合。ちょとWindowsだとわからない...)

以下、DBFlute補完テンプレートを入れたときの補完ショートカットの一部です。

Lambda, Inline Style
_li (あんすこえるあい)
Lambda, Block Style
_ll (あんすこえるえる)

特に、_li や _ll は、1.1.x (Java8版) の ConditionBean を実装する上で、非常に大切な補完なので、ぜひ使いこなしていきましょう。

Java8版の基本仕様 (1.1.x)

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

Java6版の基本仕様 (1.0.x)

Java8版と違う部分だけ記述します。

nullや空文字の扱い
基本的に 条件値が null もしくは空文字の場合は設定された条件は無効(条件付与しない) になります。条件値がリストの場合で、リストの中の null や空文字も同様、有効な条件が何もない空リストも同様。 ただし、無効な条件値を例外にするオプション、および、空文字を条件値として取扱うオプションもあります。
同じカラムで同じ比較条件の複数指定
基本的に 上書き ですが、比較条件次第で追加条件となることもあります。
同じカラムで同じ比較条件の同じ条件値での複数指定
基本的に 重複した条件にはならずにデバッグログが出力されますが、比較条件次第で追加条件となることもあります。

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_LikeSearch("S", op -> op.likePrefix());
        setMemberStatusCode_Equal_Formalized();
        existsPurchase(purchaseCB -> {
            purchaseCB.query().setProductId_Equal(SPECIAL_PRODUCT_ID);
        });
    }
}