Effective ConditionBean

コードスタイル

変数名は cb で

みなそうしています。

変数名は cb で @Java
MemberCB cb = new MemberCB();

SubQueryは subCB, OrScopeQueryは orCB など、コールバックの変数は補完されたままのもので。

SubQueryは subCB で @Java
cb.query().existsPurchaseList(new SubQuery<PurchaseCB>() {
    public void query(PurchaseCB subCB) {
        subCB.query().setPurchasePrice_GreaterEqual(2000);
    }
});

一つのメソッド内に、複数の ConditionBean を宣言する必要がある場合、つまり二回検索するような場合は、そもそもメソッド分けすると良いでしょう。 (できれば、くらいのニュアンスで)

実装順序は、データの取得、絞り込み、並び替え

みながそうすれば、可読性が良くなります。(ぜひとも、くらいのニュアンスで)

データの取得、絞り込み、並び替え @Java
MemberCB cb = new MemberCB();
cb.setupSelect_MemberStatus(); // データの取得
cb.query().setMemberName_PrefixSearch("S"); // データの絞り込み
cb.query().addOrderBy_MemberId_Asc(); // データの並び替え

// 並び替えの後に絞り込みとかやらない
//cb.query().setBirthdate_IsNull();

// データの取得は一番最初がうれしい
//cb.setupSelect_MemberSecurityAsOne();

// リスト検索
ListResultBean<Member> memberList = memberBhv.selectList(cb);

実装判断

不要なif文を減らそう

条件値がnullや空文字の場合は条件が無視されます。検索画面の検索条件などにぴったりフィットします。 この特性をうまく活かしてif文を減らして可読性を良くしましょう。

nullや空文字なら無視されるので、if文は要らない @Java
cb.query().setMemberId_Equal(memberId);
cb.query().setMemberName_PrefixSearch(memberPrefix);

// selectedStatusCode が null なら status も null
CDef.MemberStatus status = CDef.MemberStatus.codeOf(selectedStatusCode);
cb.query().setMemberStatusCode_Equal_AsMemberStatus(status);

ただし、ExistsReferrerなど条件が無い場合はサブクエリごと消えて欲しい場合は、やはりif文が必要なので、何も考えずにif文を外し過ぎないようにしましょう。

e.g. ExistsReferrerの中の条件がなくなったらサブクエリごと無くすif文 @Java
if (purchasePriceFrom != null) {
    cb.query().existsPurchaseList(new SubQuery<PurchaseCB>() {
        public void query(PurchaseCB subCB) {
            subCB.query().setPurchasePrice_GreaterEqual(purchasePriceFrom);
        }
    });
}

必須なら引数チェックをしよう

逆に、条件値が存在することが前提であれば、メソッドの引数チェックをしましょう。でないと、万が一 null や空文字が来た場合、予期せぬ大量データを検索してしまう可能性があります。

e.g. 会員名称と購入価格の検索条件の必須チェックをメソッドの先頭で @Java
/**
 * Select the list of high class members.
 * @param memberName The member name for prefix-search. (NotNull, NotEmpty)
 * @param purchasePriceFrom The purchase price for greater-equal. (NotNull)
 * @return The list of member entities. (NotNull)
 */
public List<Member> selectHighClassMemberList(
                  String memberName
                , final Integer purchasePriceFrom) {
    if (memberName == null || memberName.length() == 0) {
        throw new IllegalArgumentException("The argument 'mem..." + me...);
    }
    if (purchasePriceFrom == null) {
        throw new IllegalArgumentException("The argument 'pur...");
    }
    MemberCB cb = new MemberCB();
    cb.query().setMemberName_PrefixSearch(memberName);
    cb.query().existsPurchaseList(new SubQuery<PurchaseCB>() {
        public void query(PurchaseCB subCB) {
            subCB.query().setPurchasePrice_GreaterEqual(purchasePriceFrom);
        }
    });
}

。。。確かに、ちょっと面倒かもしれませんね(それでも、やるべきだとは思いますが)。

チェックはしたいけど、そこまでしてやりたくないという場合は、cb.checkInvalidQuery() が利用できるかもしれません。ConditionBeanの "nullや空文字で条件設定を無視する" という挙動を変更して、例外にすることができます。

e.g. 会員名称と購入価格の検索条件の必須チェックを checkInvalidQuery() で @Java
/**
 * Select the list of high class members.
 * @param memberName The member name for prefix-search. (NotNull, NotEmpty)
 * @param purchasePriceFrom The purchase price for greater-equal. (NotNull)
 * @return The list of member entities. (NotNull)
 */
public List<Member> selectHighClassMemberList(
                  String memberName
                , final Integer purchasePriceFrom) {
    MemberCB cb = new MemberCB();
    cb.checkInvalidQuery();
    cb.query().setMemberName_PrefixSearch(memberName);
    cb.query().existsPurchaseList(new SubQuery<PurchaseCB>() {
        public void query(PurchaseCB subCB) {
            subCB.query().setPurchasePrice_GreaterEqual(purchasePriceFrom);
        }
    });
}

ConditionBeanの処理の前にフィルター処理するとか別途ロジックが入るようなケースでは、やはり普通にif文でチェックすることになりますので、ケースバイケースで使い分けてください。 (よくわからないのであれば、これは使わずに全部if文で明示的にチェックする方がよいでしょう)

また、すべてのケースで適用するというより、"あっ、これちょっとチェックしておいた方がいいかなー" と、ふと頭に電流走ったときに使うと良いでしょう。

queryUpdate()で全件更新を防止

リスト検索してfor文で回って、(同じ値で、かつ、排他制御なしで)一括更新もしくは一括削除するくらいなら、queryUpdate() や queryDelete() を使いましょう。パフォーマンス的なメリットもそうですが、安全性も違います。

アンチパターンが以下のようなケースです。

e.g. これはやって欲しくない! @Java
MemberCB cb = new MemberCB();
cb.query().setMemberId_InScope(memberIdList); // 空っぽだと条件なくなる
ListResultBean<Member> memberList = memberBhv.selectList(cb);
for (Member member : memberList) {
    memberBhv.deleteNonstrict(member); // これはやってはいけない!
}

この場合、万が一 memberIdList が空リストになったら、なかなかのジ・エンドです。そもそも memberIdList は、引数チェックなどでチェックされるべき、もしくは、checkInvalidQuery() でチェックされるべきですが、やってなかったらOUTです。

でも、queryUpdate(), queryDelete() であれば、"全件更新、全件削除は許されていない" ので例外として検知できます。

e.g. queryDelete()であれば "全件更新、全件削除は許されていない" ので最悪のケースは防げる @Java
MemberCB cb = new MemberCB();
cb.query().setMemberId_InScope(memberIdList); // 空っぽだと条件なくなる
memberBhv.queryDelete(cb); // 全件削除だったら例外に

ただ、もしその InScope の条件以外に、固定の絞り込み条件がある場合は意味がありません。InScopeが消えても全件ではないので DBFlute も例外にすることができません。でも、業務的にはOUTであることには変わりないので、基本的には条件値(memberIdList)を事前にチェックするか、checkInvalidQuery() を使うことが大切です。(両方やっておくのが吉。更新や削除で何かあると大事故です)

e.g. 固定条件があると全件チェックできないので、やはり事前チェックはやっておいた方がいい @Java
MemberCB cb = new MemberCB();
cb.checkInvalidQuery();
cb.query().setMemberId_InScope(memberIdList); // 空っぽなら例外
cb.query().setMemberStatusCode_Equal_正式会員(); // 固定条件
memberBhv.queryDelete(cb);

再利用メソッド

再利用は細切れに (ArrangeQuery)

検索をまるごと再利用すると、画面間の要求のちょっとしたギャップで分岐がカオスになりがちですし、setupSelect が最小公倍数の指定になって、無駄なデータ取得が発生しやすいです。検索は、絞り込み条件などを細かく分けて再利用する方が扱いやすく効果的です。

ジェネレーションギャップを有効活用して、ConditionBean や ConditinoQuery の "Exクラス" に Arrange メソッドを定義して再利用しましょう。(ArrangeQueryと呼ぶ)

e.g. サービス会員: ある特別な商品を購入したことのある会員名称が "S" で始まる正式会員 @Java
public class MemberCQ extends BsMemberCQ {
    ...

    /**
     * Arrange the query for selecting service members. <br />
     * <pre>
     * o starts with 'S'
     * o status 'Formalized'
     * o exists the special product
     * </pre>
     */
    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);
            }
        });
    }
}
e.g. サービス会員を検索 @Java
MemberCB cb = new MemberCB();
cb.query().arrangeServiceMember();
e.g. サービス会員の購入を検索 @Java
PurchaseCB cb = new PurchaseCB();
cb.query().queryMember().arrangeServiceMember();

ConditinoQuery の方が、基点テーブルがどこであるかに依存せず再利用できるのでベターです。 ですが、OrScopeQuery や ColumnQuery などは "ConditionBeanドリブン" なので、それらを使う場合は ConditionBean にて。

再利用メソッドの名前は arrangeXxx()

arrangeXxx() という名前がオススメです。みなで合わせれば、補完するときにとりあえず arr で探せば簡単に見つけることができます。(再利用のポイントは、いかに知ってもらうか)

(Specify)DerivedReferrer や LoadReferrer のときでも、(特に違和感なければ) arrangeXxx() で良いでしょう。(ここはまあ、好みにおまかせします、くらいのニュアンスで)

e.g. 最終ログイン日時を取得する再利用メソッド @Java
public class MemberCB extends BsMemberCB {
    ...

    /**
     * Arrange the (Specify)DerivedReferrer for latest login datetime.
     * @return The alias name of the derived column. (NotNull)
     */
    public  String arrangeSpecifyLatestLoginDatetime() {
        String alias = Member.ALIAS_latestLoginDatetime;
        cb.specify().derivedMemberLoginList().max(new SubQuery<MemberLoginCB>() {
            public void query(MemberLoginCB subCB) {
                subCB.specify().columnLoginDatetime();
            }
        }, alias);
        return alias; // order by で使うかもしれないので戻しておく
    }
}

参考までにこんな命名はいかがでしょう?

jfluteがやっている命名です。 業務次第でアドリブもありますが、おおまかにはこういった名前を心がけています。 とにかく、見つけやすさ重視です。(絞り込みとソートは CB, CQ 共通)

setupSelect...() の再利用
arrangeSetupXxx()
specify() の再利用
arrangeSpecifyXxx()
query() (絞り込み) の再利用
arrangeXxx() ★いちばんよく使うので業務名だけで
query() (ソート) の再利用
arrangeOrderXxx()

再利用メソッドには必ずJavaDocを

黙っているメソッドは不安を与え、使ってくれません。

再利用を凝りすぎない

完璧な再利用を目指してはいけません。

再利用が破綻するよりかは、少しだけでもいいので "確実で安全な再利用" の方が効果があります。