DBFluteでのJava8妄想、やっぱりOptional

DBFluteメーリングリストDBFluteユーザーの集い」にて、
「DBFlute-1.1というかJava8の妄想スレッド」
という投稿をさせて頂きましたが、
そろそろ現実味が帯びてきたので、
もっと多くに人に見てもらおうと続きはブログにて。

とりあえず LoadReferrer の孫テーブル取得

とりあえず、投稿した内容の復習ですが、
memberBhv.loadPurchaseList(memberList, cb -> {
    cb.query().addOrderBy_...();
}).withNestedList(purchaseList -> {
    purchaseBhv.loadPurchaseDetailList(purchaseList, cb -> {
        cb.query().addOrderBy_...();
    });
});
要は LoadReferrer で孫テーブル(one-to-many-to-many)
を取得するときのやり方を調整します。

これは、やろうと思っています!

さて、今日のお題「Optionalの適用範囲」

さて今回は、Optional の適用範囲について考えています。

「どこまで?どういう風に?」

やはり、DBFluteだとデータベースのデータとのやり取りがポイントです。

SQL次第で、戻り値がnullがありえたりありえなかったり。
業務的にはありえないけどすれ違い更新やバグでありえたり。
通常のプログラミングとは考慮するポイントが変わってくるので、
一般的な Optional の捉え方とは少し違う感じになるかなと...

また、フレームワークの中とかじゃなくって、
不特定多数のスキルセットがバラバラな方々の
業務プログラミングにて利用する部分となります。

それらをしっかり踏まえて考えて仕様を考える必要があるかと。

Entityのプロパティの型

もし、Entityのプロパティの型を全て Optional にしたら...
Optional<String> optMemberName = member.getMemberName();
Optional<Date> optBirthdate = member.getBirthdate();
うーん、って感じですね。
さすがにうっとおしいのであんまり浸透しなさそうです。

他のO/Rマッパーがどう出てくるかちょっと様子見ですね。
こないだDomaの作者の方とお話する機会があったのですが、
同じようなことを仰られていました。
さすがにプロパティの型を全てOptionalは現実的ではなく、
Scalar値の戻り値とかで使うくらいがちょうどいいのかもと。
(アドバイス頂いちゃいました、ありがとうございます)

ということで、
まあ、dfpropの設定とかで選択できるようにはしてもいいのかもですが、
とりあえず様子見です。(現時点ではやらなそう)

scalarSelect()

こういった単発のScalar戻り値では大活躍するのかなぁと。
Optional<Date> optDate =
            memberBhv.scalarSelect(Date.class).max(cb -> {
    cb.specify().columnBirthdate();
    cb.query().setMemberStatusCode_Equal_Formalized();
});
optDate.ifPresent(birthdate -> { ... });
coalesce()を使っていれば、"問答無用get()" でもいいのかもですね。

※問答無用get()とは、isPresent()でチェックせずにいきなりget()
Optional<Date> optDate =
            memberBhv.scalarSelect(Date.class).max(cb -> {
    cb.specify().columnBirthdate();
    cb.query().setMemberStatusCode_Equal_Formalized();
}, new ScalarSelectOption().coalesce("1192-01-01"));
Date maxBirthdate = optDate.get();
nullがありえないなら別に、
ifPresent() でも構わないと考えることもできますが、
万が一それが間違っていた場合に、素通りしちゃうのもよくないなぁと。
ifPresent() は、あくまで if (date != null) {} の代わりであって、
万が一nullだったときは例外になってほしい場面では使えないのかなと。
そして、データベースプログラミングの場合はそういうケースが多いかなと。

nullがあり得ないなら、
「orElseThrow()で念のための例外をthrowしてもらう」
ってのが理想かもしれませんが、
呼び出し側でそれをやってもらうことはなかなか期待できないかなと。
例外をthrowしてもらっても、ConditionBeanの条件などを
メッセージにしっかり入れてもらわないと、あまり意味がないですし。

なので、やはりget() なのかもしれませんが、やはりここでも同じように、
間違いのときの例外は、NoSuchElementException じゃなくて、
ConditionBean の toString() とかを出したいところですね。
じゃあ、やっぱり orElseThrow() を...ぐるぐる無限ループ(><

ということで、OptionalScalar とか独自のクラスを作って、
(Optional は final クラスなので継承はできません)
null の状態で get() されたときには、
DBFluteで用意したわかりやすい例外を出すようにしようかなと。

というか、それだったら別に Java8 じゃなくてもできることじゃん!?
って感じですが、
Java8 で Optional が導入されたからこそ、
Optional 的な考え方を受け入れてもらえるとも考えられるので、
やっぱり Java8 ならではの改善と言えるのかなと。
(Java6, 7のときに同じことやっても、何これ?みたいな感じでしょう)

selectEntity()

クライマックスです。これが一番悩んでます。
現状のselectEntity()
Entity selectEntity() : 無ければ null を戻す
Entity selectEntityWithDeletedCheck() : 無ければ例外

存在しないかもしれないなら selectEntity()
 -> if文で分岐して、存在するときとしないときの処理を分ける

業務的に絶対に存在するなら selectEntityWithDeletedCheck()
 -> 万が一バグとか勘違いなら EntityAlreadyDeletedException
 -> すれ違い更新のときの排他制御もこちら
// 存在しないかもしれないなら一件検索
Member member = selectEntity(cb);
if (member != null) {
    // 存在した場合の処理
} else {
    // 存在しなかった場合の処理
}

// 業務的には必ず存在する一件検索
// (バグとかデータ不整合のときは、わかりやすい例外throw)
// (すれ違い更新のときは、排他制御例外throw)
Member member = selectEntityWithDeletedCheck(cb);
... = member.getMemberName();
と、業務的に絶対に存在するのかしないのかで、
メソッドで使い分けてもらうようにしていますが、
実際あんまりうまく使いこなされてないかなぁ!?
って気持ちがあります。

現場のコードを見ていると...

selectEntityWithDeletedCheck()
の存在に気付かずにselectEntity()するケース。
というか、
nullの可能性とか何も気にせずselectEntity()するケース。

このどちらかのケースをよく見かけます。
(大抵は大丈夫だけど、時々NullPointerみたいな)

これ逆にすればよかったかなぁとか思ったり、
(selectEntity()が例外で、
 selectEntityNullIfNoData()とか)

もっと短い名前にすればよかったかなぁとか思ったり、
(selectEntityWithDeletedCheck()じゃなくて
 selectEntityChecked()とか)

ベタベタな名前の方がよかったかなぁとか思ったり、
(selectEntityWithDeletedCheck()じゃなくて
 selectEntityIfNullException()とか)

基本メソッドなので気軽に変更することもできませんので、
しっかり伝えて使いこなしてもらうことに注力してきましたが、
何年も何回もふと後悔の念がよぎるところではありました。
というわけで今後のselectEntity()
いっそ、selectEntity()というメソッド一つだけにしちゃうとか!?
そんなこと考えています。
OptionalEntity<Member> optMember = memberBhv.selectEntity();

// 存在しないかもしれないときで、存在するときだけ処理する場合
optMember.ifPresent(() -> { ... }); // 存在するときだけコールバック実行

// 存在しないかもしれないときで、存在しないときも処理する場合
if (optMember.isPresent()) { // 存在する
    Member member = optMember.get();
    ...
} else { // 存在しない
    ...
}

// 必ず存在する場合
// (万が一なければ、EntityAlreadyDeletedException)
Member member = optMember.get();
たぶん、何も考えていないケースとか、気付かないケースであれば、
多くの人がとりあえず get() するのかなぁと想像しています。

なので、get() の例外が、
EntityAlreadyDeletedException になればいいのかなと。
(なので、Optional じゃなくて独自の OptionalEntity)

ちなみに、Optionalには、ifAbsent() がないので(恐らく)、
存在しなかったときの処理も行いたいときはやはり、
isPresent()にお世話になるでしょうか。。。
そういえば、Entityの関連テーブルは?
それにしても、Entityの関連テーブルの取得はどうでしょう!?
Member member = ...
Optional<...> optStatus = member.getMemberStatus();
Optional<...> optSecurity = member.getMemberSecurityAsOne();
うーむー、どうでしょう。。。
論理的にはここも Optional が大活躍しそうなところではあります。

 o setupSelectしたのかしてないのか?
 o NotNullのFKカラムなのか?
 o カージナリティが「1 : 0..1」なのか?

それ次第で null だったり null じゃなかったり意識すべきところ。
でもこれは勇気が要りますね。ふーむー。
やるんだったら、one-to-manyの方も同じようにやるべきだろうし、
って考えると線引きが難しいですね。。。悩んでます。

Java8互換モードを1.0.x系に用意しようかなと

すでに DBFlute を Java8 で使おうとしている人は
いらっしゃいますでしょうか?

DBFlute-1.1 に早く着手したいとは思うのですが、
さすがにすぐには出せないとは思います。

そこで、先立って1.0.x系でJava8を使われた方々が、
1.1がリリースされたときにわりとスムーズに移行できるように、
1.0.x系で1.1っぽく書けるモードを用意しようかなと。

LoadReferrerに、selectEntity()のOptionalなど。
dfpropのオプションで生成されるような感じで。

もちろん、そのためには1.1の仕様を確定しないといけないので、
Java8妄想スレッドをもっと公開範囲広げてブログにしました。
まだまだ考えている最中です。
何か意見がありましたら遠慮なくコメントくださいませm(_ _)m。