DBFlute on Java8

ポリシー、七か条

DB変更に強いO/Rマッパー、というテーマは崩さずに...

バージョン
DBFlute-1.1からは、Java8以上のバージョン限定のものに
Java8
Java8の特性を最大限活かし、将来にわたって活躍できるものに
それまでとの互換
もちろん互換は崩すが、あまりにかけ離れたものにはしない
ConditionBean
Lambdaコールバック方式で構築、LikeSearchOptionなども同様
Entity
わりとそのまんま
Behavior
わりとそのまんま、LoadReferrerはいい感じに
プログラム型
日付に関しては、Java8のTimeAPIを利用

※一部機能は 1.0.x 系でも取り込まれていますが、このページは 1.1 を中心としたお話です。

とにかく検索は?

ConditionBean で一件検索 *selectEntity(cb)

e.g. 会員ID "1" 番で会員を一件検索 (関連テーブルの会員ステータスも一緒に取得) @Java8
...
// _/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
// 一緒に取得する関連テーブル : なし
// 絞り込み条件 : 会員IDが "1" 番である => 絶対に存在する
// _/_/_/_/_/_/_/_/_/_/
memberBhv.selectEntity(cb -> cb.acceptPK(1)).alwaysPresent(member -> {
    Integer memberId = member.getMemberId();
    String memberName = member.getMemberName();
    String memberAccount = member.getMemberAccount();
    LocalDate birthdate = member.getBirthdate(); // *Java TimeAPI
    LocalDateTime formalizedDatetime = member.getFormalizedDatetime();
}); // なければ例外 (エラーメッセージでSQLが表示される)

...
// _/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
// 一緒に取得する関連テーブル : なし
// 絞り込み条件 : 会員IDが "1" 番である => 存在しないかもしれない
// _/_/_/_/_/_/_/_/_/_/
memberBhv.selectEntity(cb -> cb.acceptPK(1)).ifPresent(member -> {
    Integer memberId = member.getMemberId();
    String memberName = member.getMemberName();
    ...
}).orElse(() -> {
    // 存在しないときの処理をここで
});
  • seleceEntity() の引数が、ConditionBeanを受け取るLambdaコールバック
  • ConditionBeanの変数名は、相変わらず cb が習慣!
  • selectEntity() の戻り値が OptionalEntity になっていて null は戻らない
  • 絶対に存在することが前提なら、alwaysPresent() ※selectEntityWithDeletedCheck()と同じ
  • 存在しないかもしれないなら、ifPresent() に続いて orElse() ※ifPresent()だけでもOK
  • Entityの変数名は、Member なら member、MemberWithdrawal なら withdrawal という感じで
  • alwaysPresent() と orElse() (ifPresent()の戻り値) はDBFlute独自のメソッド

ConditionBean でリスト検索 *selectList(cb)

e.g. 色々な絞り込み条件で会員をリスト検索 (関連テーブルもいろいろと) @Java8
...
// _/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
// 一緒に取得する関連テーブル:
//  o 会員ステータス => 絶対に存在する
//  o 会員退会情報 => 存在しないかもしれない
//  o 会員サービスと、その親テーブルのサービスランク => 絶対に存在する
// 絞り込み条件:
//  o 会員名称が "S" で始まる
//  o 会員ステータスが "正式会員" である (区分値メソッド)
//  o 支払い済みで200円以上の購入をしたことがある会員 (one-to-many)
// ソート条件:
//  o 生年月日の降順 (でも null は後ろにね)
//  o 会員IDの昇順
// _/_/_/_/_/_/_/_/_/_/
ListResultBean<Member> memberList = memberBhv.selectList(cb -> {
    cb.setupSelect_MemberStatus();
    cb.setupSelect_MemberServiceAsOne().withServiceRank();

    // LikeSearchOptionだって、Lambdaコールバック
    cb.query().setMemberName_LikeSearch("S", op -> op.likePrefix());
    cb.query().setMemberStatusCode_Equal_Formalized();

    cb.query().existsPurchase(purchaseCB -> {
        purchaseCB.query().setPurchasePrice_GreaterEqual(200);
        purchaseCB.query().setPaymentCompleteFlg_Equal_True();
    });

    cb.query().addOrderBy_Birthdate_Desc().withNullsLast();
    cb.query().addOrderBy_MemberId_Asc();
}); // そのまま回すだけなら、ここから forEach() や stream() しちゃってもOK

memberList.forEach(member -> { // でもまあ、とりあえず変数に受け取ってから回してみる
    Integer memberId = member.getMemberId();
    String memberName = member.getMemberName();
    ...

    // 関連テーブルの Entity も OptionalEntity (カージナリティを見てメソッド選ぶべし)
    member.getMemberStatus().alwaysPresent(status -> {
        String statusName = status.getMemberStatusName();
        ...
    });
    member.getMemberWithdrawalAsOne().ifPresent(withdrawal -> {
        LocalDateTime withdrawalDatetime = withdrawal.getWithdrawalDatetime();
        ...
    });
});
  • seleceList() の引数が、ConditionBeanを受け取るLambdaコールバック
  • ConditionBeanの変数名は、相変わらず cb が習慣!
  • LikeSearchOptionも、Lambdaコールバック (その他Optionも同様)
  • forEach()はJava8のListの標準メソッドなので、DBFlute関係ない
  • 関連テーブルも OptionalEntity になっていて、nullは戻さない

テーブル、つまり Entity は Optional 祭り ☆

selectEntity() の戻り値、そして、setupSelectしたときの関連テーブルをEntityでgetするとき、もろもろとにかく Entity は OptionalEntity になっています。

一発処理でOKスタイルを軸に、色々なスタイルに当てはめていくとよいでしょう。

一発処理でOKスタイル
その場で利用しておしまいなケース
なんども登場スタイル
そのあと何度も利用するようなケース
みんなで勢ぞろいスタイル
入りみだれて利用するようなケース
トラディショナルスタイル
いざとなったら or 迷って手が止まるくらいだったら
スタイル入りみだれスタイル
その名の通り

ただ、カラムは Optional ではありません。少なくとも Optional という概念が導入されたばかりの Java の世界では、周りのプログラムと合わない部分が多く発生して、デメリットの方が上回ってしまうと判断しました。 NotNull制約の変更との兼ね合いも考えて、

Optionalの例外メッセージをどうにかしたかった ☆

Entity の Optional はJava8標準ではなく、DBFlute独自の OptionalEntity というクラスです。 もちろん、できれば標準のものを使いたかったのですが、大きなきっかけは例外メッセージです。

データベースの検索結果は、絞り込み条件や業務状況が変われば、"存在する/しない" が変わります。あらかじめ "存在するからそのまま、nullがありえるからOptional" というような static な作りはできません。

なので、絶対に存在するケースでも Optional のままで受け取ることがあります。そのときは、ノーチェックで alwaysPresent() か get() するでしょう。とはいえ、データベースに絶対はありません。そう、DBの世界で "絶対に存在する" というのは、業務的に存在するという意味であり、業務上の例外(すれ違い)や、データバグなどがありえるのです。開発中でも本番でも。そのときに単なる NoSuchElementException ではつらいなと。 (ifPresent()で素通りして全然違う例外が出てもつらいし、orElseThrow()で毎度毎度チェックロジックを書くのもつらい...し、なかなかやってもらえないでしょう)

ということで、selectEntity() の OptionalEntity では、EntityAlreadyDeletedException で発行した SQL がエラーメッセージに載るようにしています。 その他の場面でも、いい感じの例外が出るようにして、不意の状況が発生したときにでもできるだけデバッグがしやすいようにしています。

一方で、標準の Optional とほぼ同じメソッドを用意しているので、全く同じように使えます。 さらに、Entityを取り扱うのに適したメソッドをちょっとだけ追加しています。alwaysPresent()がまさしくそうですね。

その他のConditionBeanによる検索

同じような感じで Lambda になるだけで、そんなに変わりません。

selectCount(cb)
本当に、Lambdaになっただけ
selectPage(cb)
PagingResutlBeanのメソッドが改善された
selectCursor(cb)
Lambdaになっただけだけど、おかげで引数二つともコールバックに
scalarSelect(cb)
戻り値が OptionalObject に

※他の項目の Example で "ついでに" 登場しますので、そちらを参考に

さくっと (Specify)DerivedReferrer

導出カラム用の ExEntity の プロパティ (Getter/Setter) を作らなくても取得できます。

e.g. 会員名称に "vi" が含まれる会員をページング検索 @Scala
...
// _/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
// 一緒に取得する関連テーブル
//  o 最終ログイン日時
//  o 支払済みである購入の平均購入価格
// 絞り込み条件
//  o 購入に対する支払で、一回で3000円以上もの金額を支払ったことがある会員
// ソート条件
//  o 最終ログイン日時の降順
// _/_/_/_/_/_/_/_/_/_/
String keyOfLatestLoginDatetime = "$LATEST_LOGIN_DATETIME"; // "$" がポイント
String keyOfPurchasePriceAverage = "$PURCHASE_PRICE_AVERAGE";

// ページング検索は、完全に "ついでに" というかんじ
PagingResultBean<Member> page = memberBhv.selectPage(cb -> {
    cb.specify().derivedMemberLoginList().max(loginCB -> {
        loginCB.specify().columnLoginDatetime()
    }, keyOfLatestLoginDatetime);
    cb.specify().derivedPurchase().avg(purchaseCB -> {
        purchaseCB.specify().columnPurchasePrice();
        purchaseCB.query().setPaymentCompleteFlg_Equal_True();
    }, keyOfPurchasePriceAverage);
    cb.query().existsPurchase(purchaseCB -> {
        purchaseCB.query().existsPurchasePaymentList(paymentCB -> {
            paymentCB.query().setPaymentAmount_GreaterEqual(3000);
        });
    });
    cb.query().addSpecifiedDerivedOrderBy_Desc(keyOfLatestLoginDatetime);
}

page.forEach(member -> {
    ... = member.getMemberName();

    // derived()メソッドで、Mapっぽく取れる
    // 型は、以下のようなルールで決まる
    //  o count()      : Integer
    //  o max(), min() : 対象カラムのプロパティ型と同じ
    //  o sum(), avg() : BigDecimal
    LocalDateTime latestLoginDt = member.derived(keyOfLatestLoginDatetime);
    BigDecimal averagePrice = member.derived(keyOfPurchasePriceAverage);
    ...
}
int allRecordCount = page.getAllRecordCount();
int allPageCount = page.getAllPageCount();
if (page.existsPreviousPage()) {
    ...
}
if (page.existsNextPage()) {
    ...
}
List<Integer> numberList = page.buildRangeNumberList(op -> op.rangeSize(5));
...

もちろん、ExEntityにプロパティを定義する今までのやり方も使えます。

特に、どちらでないとよくないということはありません。 ただ、アプリの共通クラスである Entity に各画面のディベロッパーが気軽にメソッドを追加できないケースもあるので、Entityの修正なしで気軽に DerivedReferrer できるインターフェースが必要であるということでサポートしました。

なんとか Option がもろもろ Lambda

LikeSearchOption や FromToOption, ColumnConversionOption に ManualOrderBean などのもろもろの Option が、Lambdaコールバックになっています。

e.g. 会員名称に "vi" が含まれる会員をカーソル検索 @Scala
...
// _/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
// 一緒に取得する関連テーブル
//  o 現在の会員住所情報(業務的one-to-one)
// 絞り込み条件:
//  o 会員名称に "vi" が含まれる
//  o 二十歳になってから正式会員になった会員
// ソート条件:
//  o 2005年10月1日から3日にまでに正式会員になった人を先に並べる
//  o 会員サービスのサービスポイント数の降順
// _/_/_/_/_/_/_/_/_/_/
LocalDate fromDate = LocalDate.of(2005, 10, 1);
LocalDate toDate = LocalDate.of(2005, 10, 3);

// カーソル検索は、完全に "ついでに" というかんじ
memberBhv.selectCursor(cb -> {
    cb.setupSelect_MemberAddressAsValid(currentDate);

    // LikeSearchOption だって
    cb.query().setMemberName_LikeSearch("vi", op -> op.likeContain());

    cb.columnQuery(col -> col.specify().columnFormalizedDatetime())
        .greaterThan(col -> col.specify().columnBirthdate()) 
        .convert(op -> op.addYear(20)); // ColumnConversionOption だって

    // ManualOrderBean だって
    cb.query().addOrderBy_FormalizedDatetime_Asc().withManualOrder(mob -> {
        // FromToOption だって、みーんな Lambda
        mob.when_FromTo(fromDate, toDate, op -> op.compareAsDate());
    });
    cb.query().queryMemberServiceAsOne().addOrderBy_ServicePointCount_Desc();
}, member -> { // 第二引数でCursorHandler
    // 検索された Entity が一件ずつ落ちてくる
    String memberName = member.getMemberName();
    member.getMemberAddressAsOne().map(...);
    ...
});

また、PrefixSearch や DateFromTo などの、固定オプションのファサード的なメソッドはデフォルトで出力されなくなりました。 Lambdaの導入で Option の指定がしやすくなったことにより、LikeSearch や FromTo に統一することにしました。

子テーブルの検索 (LoadReferrer) *load(loader)

1.0.x系でもサポートされた Loader 方式全開で実装しましょう。

e.g. 会員をリスト検索して、子テーブルもいろいろと @Java8
...
// _/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
// 一緒に取得する関連テーブル:
//  o 会員ステータス (その子テーブルである会員ログインも取得される予定)
//  o 購入と商品 (購入日時の降順で) (one-to-many, one-to-many-to-one)
//  o 購入支払 (支払日時の降順で) (one-to-many-to-many)
//  o 会員ステータス経由の会員ログイン (ログイン日時の降順で) (many-to-one-to-many)
// 絞り込み条件:
//  o 会員名称が "S" で始まる
// ソート条件:
//  o 会員IDの昇順
// _/_/_/_/_/_/_/_/_/_/
ListResultBean<Member> memberList = memberBhv.selectList(cb -> {
    cb.setupSelect_MemberStatus() // 後で pullout されるので必要
    cb.query().setMemberName_LikeSearch("S", op -> op.likePrefix());
    cb.query().addOrderBy_MemberId_Asc();
});
memberBhv.load(memberList, loader -> {
    loader.loadPurchase(purchaseCB -> {
        purchaseCB.setupSelect_Product();
        purchaseCB.query().addOrderBy_PurchaseDatetime_desc();
    }).withNestedReferrer(purchaseLoader -> { // "loader" と名前がかぶらないように
        purchaseLoader.loadPurcasePaymentList(paymentCB -> {
            paymentCB.query().addOrderBy_PaymentDatetime_desc();
        }
    }
    loader.pulloutMemberStatus().loadMemberLoginList(loginCB -> {
        loginCB.query().addOrderBy_LoginDatetime_Desc();
    });
}

memberList.forEach(member -> {
    String memberName = member.getMemberName();
    member.getPurchaseList().forEach(purchase -> {
        purchase.getProduct().alwaysPresent(product -> {
            String productName = product.getProductName();
            ...
        });
        purchase.getPurchasePaymentList().forEach(payment -> {
            BigDecimal paymentAmount = payment.getPaymentAmount();
            LocalDateTime paymentDatetime = payment.getPaymentDatetime();
            ...
        });
    });
    member.getMemberStatus().alwaysPresent(status -> {
        status.getMemberLoginList().forEach(login -> {
            LocalDateTime loginDatetime = login.getLoginDatetime();
            ...
        });
    });
});

そういえば更新とかは?

ほとんど変わりません。今まで通り、Entity を new して、set/set してください。

queryUpdate()は?

ただ、queryUpdate() や queryDelete() などの ConditionBean を受け取るメソッドは、検索と同様にLambdaコールバックになっています。

varyingUpdate()は?

また、varyingUpdate() などの Option を受け取るメソッドも、Optionオブジェクトの指定がLambdaコールバックになっています。

batchUpdate() の SpecifyColumn が...

あと、batchUpdate()の明示的カラム指定オーバーロードメソッドは、デフォルトで出力されなくなっています。 既に1.0.x系でも batchUpdate() は Setter 呼び出しされたカラムが更新されるようになっています。 それにより、オーバーロードのメソッドはあまり意味を持たなくなりましたので、シンプル化しました。

日付が LocalDate に!

マッピングされる日付が、Java8 の TimeAPI になります。

日付のマッピング

DATE
LocalDate ※いままでは、java.util.Date
DATETIME
LocalDateTime ※いままでは、java.sql.Timestamp
TIME
LocalTime ※いままでは、java.sql.Time

HandyDateは?

TimeAPI はとても便利に作られています。これにて HandyDate はお役御免となりそうですが、それでも HandyDate 特有の便利メソッドもありますので、いままで通り使えます。LocalDate との相互変換のメソッドも用意されていますので、時に使ってみるといいでしょう。

TimeZoneは?

LocalDateは、西暦表現の文字列をオブジェクトにしたようなものです。取り扱いに日付変換が欠かせません。 DBFluteの中で、日付変換のときに使うTimeZoneを個別個別、もしくは、一括で設定できるようにしています。

さあ、補完してみよう!

今までと、補完の仕方が変わるので、ちょっと指のことを考えてあげましょう。

デフォルトの Eclipse なら

まずは、デフォルトの Eclipse (執筆時点で4.4.1) の補完で書くなら...

ひとことで言うと... "Lambda" 部分を消して矢印!

e.g. 色々な絞り込み条件で会員をリスト検索 (関連テーブルもいろいろと) @Java8
// まず、ConditionBeanをnewするじゃなくて、基点テーブルのBehaviorを選ぶのがコツ
// まず、sel まで打って、大文字を狙って...キャメルケースコード補完
memberBhv.selL // selL -> ctrl+space
--
memberBhv.selectList(cbLambda) // cbLambda が選択状態に
--
memberBhv.selectList(cb) // cbLambda の "Lambda" を消して...
--
memberBhv.selectList(cb -> {) // " -> {" と打って enter
--
memberBhv.selectList(cb -> {
    // カーソルはここ
}) // セミコロンないんだけど後でね
--
memberBhv.selectList(cb -> {
    // ここはいつも通りの補完 cb.q => cb.query() => cb.query().setMNLS という流れ
    cb.q
})
--
memberBhv.selectList(cb -> {
    // この時点で、"memberName" が選択状態に、tabで "opLambda" を行き来できる
    cb.query().setLikeSearch_LikeSearch(memberName, opLambda);
})
--
memberBhv.selectList(cb -> {
    // まあ、第一引数は "S" と打って tab 押して "opLambda" を選択状態に
    cb.query().setLikeSearch_LikeSearch("S", opLambda);
})
--
memberBhv.selectList(cb -> {
    // opLambda の "Lambda" を消して...
    cb.query().setLikeSearch_LikeSearch("S", op);
})
--
memberBhv.selectList(cb -> {
    // " -> op." と打って likePrefix() を選択 (enterすれば一番右にカーソルが飛ぶ)
    cb.query().setLikeSearch_LikeSearch("S", op -> op.likePrefix());
})
--
// さて、書き終わったら...
memberBhv.selectList(cb -> {
    cb.query().setLikeSearch_LikeSearch("S", op -> op.likePrefix());
    cb.query().addOrderBy_Birthdate_Desc();
}); // セミコロン付けてから、ここでいつもの ctrl + 2 => L
--
// どーんっと変数を補完!"selectList" 変数が選択状態に
ListResultBean selectList = memberBhv.selectList(cb -> {
    cb.query().setLikeSearch_LikeSearch("S", op -> op.likePrefix());
    cb.query().addOrderBy_Birthdate_Desc();
});
--
// 変数名はご自由に。ここでは memberList とベタに打って enter
ListResultBean memberList = memberBhv.selectList(cb -> {
    cb.query().setLikeSearch_LikeSearch("S", op -> op.likePrefix());
    cb.query().addOrderBy_Birthdate_Desc();
}); // すると、カーソルはここに飛ぶ
--
memberList.for // memberList の for で ctrl+space
--
memberList.forEach(action); // action が選択状態に
--
memberList.forEach(member -> {); // "member -> {" と打って enter
--
memberList.forEach(member -> {
    // カーソルがここに
});

DBFluteの中でコールバックするときは、多くの場面で xxxLambda というような引数名になっているので、Lambda部分を消して " -> {" + enter もしくは " -> xxx." と打っていけばOKです。

ですが、SubQuery の ConditionBean の名前や LoadReferrer の loader などのネストする可能性のあるものや、OptionalEntity における Entity などテーブル名のムード出したいものでは、このやり方だとちょっと面倒です。普通にベタベタ書くしかないです。

DBFlute補完テンプレートを入れたEclipse

ということで、Eclipseで補完テンプレート (Java Editor Templates) を用意しています。

_arrow (_a)
-> ※単なる矢印
_li (えるあい)
var -> var.xxx ※Lambda Inline: LikeSearch などの一行スタイル
_ll (えるえる)
var -> {改行var.xxx改行} ※Lambda Linefeed: CB などの複数行スタイル
_foreach (_fo)
var.forEach(newName -> {改行}); ※var部分は近くの変数に置き換わる
_scolist (_scol)
...am().collect(Collectors.toList());
_scomap (_scom)
...am().collect(Collectors.toMap(bean -> be...);

やってみましょう。

e.g. 色々な絞り込み条件で会員をリスト検索 (関連テーブルもいろいろと) @Java8
// まず、ConditionBeanをnewするじゃなくて、基点テーブルのBehaviorを選ぶのがコツ
// まず、sel まで打って、大文字を狙って...キャメルケースコード補完
memberBhv.selL // selL -> ctrl+space
--
memberBhv.selectList(cbLambda) // cbLambda が選択状態に
--
memberBhv.selectList(_ll) // そのまま上書き _ll (えるえる) => ctrl+space
// 補足: 他に_llで始める補完やクラスがなければ enter は不要
// 補足: L (える) を薬指で二回、ちょちょんと押すとスムーズに
--
memberBhv.selectList(var -> { // var が選択状態に
    var.object
}) // セミコロンないんだけど後でね
--
memberBhv.selectList(cb -> { // そのまま cb と打って...
    cb.object // こっちの変数も同時に cb になって...
})
--
memberBhv.selectList(cb -> { // tab (たぶ!)
    cb.object // (tabの後) すると object が選択状態に
})
--
memberBhv.selectList(cb -> {
    // ここはいつも通りの補完 cb.q => cb.query() => cb.query().setMNLS という流れ
    cb.q
});
--
memberBhv.selectList(cb -> {
    // この時点で、"memberName" が選択状態に、tabで "opLambda" を行き来できる
    cb.query().setLikeSearch_LikeSearch(memberName, opLambda);
});
--
memberBhv.selectList(cb -> {
    // まあ、第一引数は "S" と打って tab 押して "opLambda" を選択状態に
    cb.query().setLikeSearch_LikeSearch("S", opLambda);
});
--
memberBhv.selectList(cb -> {
    // そのまま上書き _li (えるあい) => ctrl+space
    cb.query().setLikeSearch_LikeSearch("S", _li);
})
// 補足: 他に_liで始める補完やクラスがなければ enter は不要
// 補足: L (える) を薬指、I (あい) を中指で "ひっかけるように" 押すとスムーズに
--
memberBhv.selectList(cb -> {
    // varが選択状態に
    cb.query().setLikeSearch_LikeSearch("S", var -> var.object);
})
--
memberBhv.selectList(cb -> {
    // そのまま op と入力すると、両方とも op に
    cb.query().setLikeSearch_LikeSearch("S", op -> op.object);
})
--
memberBhv.selectList(cb -> {
    // tab 押すよ!すると、右側のobjectにカーソルが飛ぶから...
    cb.query().setLikeSearch_LikeSearch("S", op -> op.object);
})
--
memberBhv.selectList(cb -> {
    // "li" と打って ctrl+space して likePrefix() を選択 (enterすれば一番右に)
    cb.query().setLikeSearch_LikeSearch("S", op -> op.likePrefix());
})
--
// さて、書き終わったら...
memberBhv.selectList(cb -> {
    cb.query().setLikeSearch_LikeSearch("S", op -> op.likePrefix());
    cb.query().addOrderBy_Birthdate_Desc();
}); // セミコロン既にあるので、ここでいつもの ctrl + 2 => L
--
// どーんっと変数を補完!"selectList" 変数が選択状態に
ListResultBean selectList = memberBhv.selectList(cb -> {
    cb.query().setLikeSearch_LikeSearch("S", op -> op.likePrefix());
    cb.query().addOrderBy_Birthdate_Desc();
});
--
// 変数名はご自由に。ここでは memberList とベタに打って enter
ListResultBean memberList = memberBhv.selectList(cb -> {
    cb.query().setLikeSearch_LikeSearch("S", op -> op.likePrefix());
    cb.query().addOrderBy_Birthdate_Desc();
}); // すると、カーソルはここに飛ぶ
--
_fo // memberList のすぐ下で _fo で ctrl+space
--
memberList.forEach(var -> { // varが選択状態に

});
--
memberList.forEach(member -> { // member と打って enter
    // すると、カーソルがここに
});

なんか、楽になった気がしませんね。。。

いや、実際に指を動かして比べると、だいぶ違います。少し Eclipse に振り回されてる感がありますが、意外に cbLambda の Lambda 部分を消して " -> {" と打つのがわりと面倒です。これは、使ってるキーボードによるかもしれませんが、"矢印" はスムーズに案外打ちづらいのです。(shift離してハイフン、shift押して大なり...)

ちょっと修練は必要ですが、慣れると DBFlute に限らず Lambda がスムーズに補完できるようになるので、もしよかったらぜひ活用してみてください。

補完テンプレート
dbflute-eclipse-editor-templates.zip

ダウンロードして解凍したXMLファイルを、workspaceの設定 (メニューの Windows - Preferences) の Java - Editor - Templates にて、(既存の設定を残しつつそのまま) importしてみてください。アンダースコア始まりの補完が使えるようになります。

(本当は、Eclipseのバージョンが上がって、こういった補完でデフォルトでできるようになったらいいなと...)

裏技あります

これは上級者向け、補完テンプレートを入れたら...

selectList(cbLambda) を補完後に、右押してctrl+space!

"右"って、cbLambdaの選択状態を解除して、cbLambdaの右側にカーソルを確定させているだけです。

Eclipseのご乱心に注意

Lambdaの中の、補完がまだ 4.4.1 だととても不安定です。おかしいな!?と思ったら、いったん周りのコンパイルエラーを直しましょう。 特にセミコロンの有無とか括弧とか要注意です。セミコロンを付けたら補完されるようになった とかよくあります。また、Blockスタイルの Lambda ではなく、Inlineスタイルの Lambda (一行のLambda) だと安定することもあるので、一行しかステートメントがないときは、Inlineスタイルで試してみましょう。

時間の問題と思われますが、ConditionBeanのコールバックだけは最初は Lambda を使わずにいつも通りの new[スペース] + ctrl+space enter をして、昔ながらのコールバックの中でCBの実装をしてひと段落してから、ctrl+1でLambdaに変換するでもよいでしょう。

また、Eclipseの入力モード (勝手にそう呼んでいますが、変数名やstatementが四角で囲われている状態) がどんどんネストしていって、enter押したときのカーソルの飛び先やセミコロンを入力したおきの扱いなどがわけわからないことになることがあります。

まずは、びっくりしないこと。そして、なんだか変な感じだなと思ったら、escボタンを押して入力モードを強制終了しちゃってもいいでしょう。 しかも、一回だけでなく、二、三回押すと確実にモードがおわりやすいので、escをパンパン!っと。(いずせにせよ、反射神経を研ぎすませて...)

IntelliJ IDEA なら

同じテンプレートが、IntelliJ IDEA でも使えます!

メニューの File - Import Setttings... で以下の jar ファイルを指定して Live Templates だけ選択して import すれば利用できます。 (Live Templates 以外を選択すると、ご自身の設定が上書きされる可能性があります)

補完テンプレート
dbflute-intellij-live-templates.jar

裏技は使えませんが、アンスコで始まる系の補完はほとんどそのまま利用できます。_ll, _li をぜひ活用していきましょう!Eclipseよりも使いやすくなっている部分もあります。

Deco, Hakiba, thank you for your migration from Eclipse.

たかが変数名、されど変数名

Lambdaを使うとスッキリする分、型宣言がなくなるため、名前がより重要 になります。

また、Lambdaの引数の変数名で、外側の変数を上書きできないため、何かしらユニークな名前をつけてあげる必要があります。

命名の推奨ルール

DBFluteとしては、以下のようなルールで名前を付けることを推奨しています。 みんなが同じようなルールで命名すれば、プログラムの可読性は圧倒的に高くなるはずです。

基点テーブルのCB
cb で OK
ExistsReferrer
[entity名]CB: 独立した世界のCBなら、テーブルを識別しやすく
DerivedReferrer
[entity名]CB: 上に同じ
LoadReferrer
[entity名]CB: 上に同じ ※Loaderは [entity名]Loader
ScalarCondition
[entity名]CB: 上に同じ ※PartitionByは colCB
OrScopeQuery
orCB: 一時的な役割を担うCBなら、役割prefixを
ColumnQuery
colCB: 上に同じ ※その他、カラムだけを指定するものはこれと同じ
UnionQuery
unionCB: 上に同じ
Option系
op: もうお約束で ※名前がバッティングするときは、アドリブで短い役割名を
alwaysPresent()など
[entity名]: Entity名のイメージ (わかれば短くしてもOK)
その他いろいろ
これらを踏まえて、応用していい感じの名前を

推奨ルールでの実装Example

e.g. 色々な絞り込み条件で会員をリスト検索 (関連テーブルもいろいろと) @Java8
ListResultBean<Member> memberList = memberBhv.selectList(cb -> {
    cb.setupSelect_MemberStatus();
    cb.setupSelect_MemberServiceAsOne().withServiceRank();

    cb.specify().derivedMemberLogin().max(loginCB -> {
        loginCB.specify().setLoginDatetime();
        loginCB.query().setMobileLoginFlg_Equal_False();
    }, "$latestLoginDatetime");

    cb.query().setMemberName_LikeSearch("S", op -> op.likePrefix());
    cb.query().setMemberStatusCode_Equal_Formalized();

    cb.query().existsPurchase(purchaseCB -> {
        purchaseCB.query().setPurchasePrice_GreaterEqual(200);
        purchaseCB.query().setPaymentCompleteFlg_Equal_True();
    });

    cb.orScopeQuery(orCB -> {
        orCB.query().setBirthdate_IsNull(200);
        orCB.query().setMemberId_GreaterEqual(3);
    });

    cb.columnQuery(col -> col.specify().columnFormalizedDatetime())
        .greaterThan(col -> col.specify().columnBirthdate()) 
        .convert(op -> op.addDay(7));

    cb.query().addOrderBy_Birthdate_Desc().withNullsLast();
    cb.query().addOrderBy_MemberId_Asc();
});

memberBhv.load(memberList, memberLoader -> {
    memberLoader.loadPurchase(purchaseCB -> {
        purchaseCB.query().setPurchasePrice_GreaterEqual(200);
        purchaseCB.query().addOrderBy_PurchaseDatetime_Desc();
    }).withNestedReferrer(purchaseLoader -> {
        purchaseLoader.loadPayment(paymentCB -> {
            paymentCB.query().addOrderBy_PaymentDatetime_Desc();
        });
    });
    memberLoader.pulloutMemberStatus().loadMemberLogin(loingCB -> {
        loginCB.query().addOrderBy_LoginDatetime_Desc();
    });
});

memberList.forEach(member -> {
    Integer memberId = member.getMemberId();
    String memberName = member.getMemberName();
    ...

    member.getMemberStatus().alwaysPresent(status -> {
        String statusName = status.getMemberStatusName();
        ...
    });
    member.getMemberWithdrawalAsOne().ifPresent(withdrawal -> {
        LocalDateTime withdrawalDatetime = withdrawal.getWithdrawalDatetime();
        ...
    });
});

1.1 からはより安心、安全を

勇気を出して変わったところがあります。

Strict: nullOrEmpty

CBでnullや空文字入れたら例外に! (オプションで戻せます)

Strict: SpecifyColumn

CBでSpecifyColumnしてないカラムをgetしたら例外に! (オプションで戻せます)

1.0.x から 1.1 への移行

1.0.x から 1.1 へ移行するためのドキュメントが用意されています。