ハンズオンセクション 4

概要

さて、ハンズオンの続きです。

"ConditionBeanでタイプセーフ区分値" を学んでいきましょう。

事前準備

org.docksidestage.handson.exercise.HandsOn04Test クラスを作成してください。 このクラスは UnitContainerTestCase を継承します。また、ERDを開いておくと良いでしょう。

ベタベタのやり方

以下のテストを作成してください。

退会会員の未払い購入を検索
  • 退会会員のステータスコードは "WDL"。ひとまずベタで
  • 支払完了フラグは "0" で未払い。ひとまずベタで
  • 購入日時の降順で並べる
  • 会員名称と商品名と一緒にログに出力
  • 購入が未払いであることをアサート
会員退会情報も取得して会員を検索
  • 退会会員でない会員は、会員退会情報を持っていないことをアサート
  • 退会会員のステータスコードは "WDL"。ひとまずベタで
  • 不意のバグや不意のデータ不備でもテストが(できるだけ)成り立つこと

区分値メソッドの生成と活用

DBFluteでは、区分値に対してタイプセーフなアプローチができます。

DBFluteプロパティの設定

まずは、区分値を定義してみましょう。DBFluteプロパティである classificationDefinitionMap.dfprop (DBFluteクライアント/dfprop配下) に、以下の定義を設定してください。現場フィットの区分値のページを参考にすると良いでしょう。

e.g. フラグと会員ステータスの区分値を定義 @classificationDefinitionMap.dfprop
 ...
    ; Flg = list:{
        ; map:{topComment=フラグを示す; codeType=Number}
        ; map:{code=1; name=True ; alias=はい  ; comment=有効を示す}
        ; map:{code=0; name=False; alias=いいえ; comment=無効を示す}
    }
    ; MemberStatus = list:{
        ; map:{topComment=入会から退会までの会員のステータスを示す; codeType=String}
        ; map:{
            ; table=MEMBER_STATUS
            ; code=MEMBER_STATUS_CODE; name=MEMBER_STATUS_NAME
            ; comment=DESCRIPTION; orderBy=DISPLAY_ORDER
        }
    }
 ...

暗黙の区分値であるフラグ(Flg)は、どのテーブルのどのカラムに関連付けるのかを定義する必要があります。 同じくDBFluteプロパティである classificationDeploymentMap.dfprop に、以下の定義を設定してください。

e.g. フラグ(Flg)を "_FLG" で終わるカラム全てに関連付け @classificationDeploymentMap.dfprop
 ...
    ; $$ALL$$ = map:{suffix:_FLG=Flg}
 ...

再自動生成 (Doc and Generate)

では、DocタスクとGenerateタスクを実行してください。SchemaHTMLにて、関連付いたカラムの Classification 欄に区分値の名前がリンクになって表示されているはずです。 そのようになっていれば設定がうまくいった証拠で、プログラム上でも区分値情報を利用した実装ができるようになります。

ベタベタなやり方を修正

それでは、先ほど実装したベタベタなやり方を修正してみましょう。

そもそも、Java8 (1.x) であれば、コンパイルエラーになっているはずです。

さらに区分値を活用して実装

それではさらに区分値メソッドに慣れていきましょう。

他の区分値も定義して再自動生成

以下の他の区分値も定義して再自動生成してください。(生成後、SchemaHTMLを確認)

その他の区分値の定義 @classificationDefinitionMap.dfprop
 ...
    ; ServiceRank = list:{
        ; map:{topComment=会員が受けられるサービスのランクを示す; codeType=String}
        ; map:{
            ; table=SERVICE_RANK
            ; code=SERVICE_RANK_CODE; name=SERVICE_RANK_NAME
            ; comment=DESCRIPTION; orderBy=DISPLAY_ORDER
        }
    }
    ; Region = list:{
        ; map:{topComment=主に会員の住んでいる地域を示す; codeType=Number}
        ; map:{
            ; table=REGION
            ; code=REGION_ID; name=REGION_NAME
            ; orderBy=REGION_ID
        }
    }
    ; WithdrawalReason = list:{
        ; map:{topComment=会員の退会理由。なのでちょっとねがてぃぶ; codeType=String}
        ; map:{
            ; table=WITHDRAWAL_REASON
            ; code=WITHDRAWAL_REASON_CODE; name=WITHDRAWAL_REASON_CODE
            ; comment=WITHDRAWAL_REASON_TEXT; orderBy=DISPLAY_ORDER
        }
    }
    ; ProductCategory = list:{
        ; map:{topComment=商品のカテゴリ。階層構造である; codeType=String}
        ; map:{
            ; table=$sql:
 PRODUCT_CATEGORY loc left outer join PRODUCT_CATEGORY rel on loc.PARENT_CATEGORY_CODE = rel.PRODUCT_CATEGORY_CODE
            ; code=$sql: loc.PRODUCT_CATEGORY_CODE
            ; name=$sql: loc.PRODUCT_CATEGORY_NAME
            ; comment=$sql: 'of ' || rel.PRODUCT_CATEGORY_NAME
            ; orderBy=loc.PARENT_CATEGORY_CODE is not null, loc.PARENT_CATEGORY_CODE
        }
    }
    ; ProductStatus = list:{
        ; map:{topComment=商品ステータス。あんまり面白みのないステータス; codeType=String}
        ; map:{
            ; table=PRODUCT_STATUS
            ; code=PRODUCT_STATUS_CODE; name=PRODUCT_STATUS_NAME
        }
    }
    ; PaymentMethod = list:{
        ; map:{
            ; topComment=支払方法; codeType=String
            ; isCheckImplicitSet=true
            ; groupingMap = map:{
                ; recommended = map:{
                    ; groupComment = 最も推奨されている方法
                    ; elementList = list:{ByHand}
                }
            }
        }
        ; map:{code=HAN; name=ByHand; alias=手渡し; comment=Face-to-Faceの手渡しで商品と交換}
        ; map:{code=BAK; name=BankTransfer; alias=銀行振込; comment=銀行振込で確認してから商品発送}
        ; map:{code=CRC; name=CreditCard; alias=クレジットカード; comment=クレジットカードの番号を教えてもらう}
    }
 ...
その他の区分値の関連付け @classificationDeploymentMap.dfprop
 ...
    ; PURCHASE_PAYMENT = map:{PAYMENT_METHOD_CODE=PaymentMethod}
 ...

区分値メソッドを使って実装

それでは、区分値メソッドを大いに活用して、以下のテストを作成してください。

一番若い仮会員の会員を検索
  • 区分値メソッドの JavaDoc コメントを確認する
  • 会員ステータス名称も取得する(ログに出力)
  • 会員が仮会員であることをアサート
※できれば、テストメソッド内の検索回数は一回で...
支払済みの購入の中で一番若い正式会員のものだけ検索
  • 会員ステータス名称も取得する(ログに出力)
  • 購入日時の降順で並べる
  • 購入の紐づいている会員が正式会員であることをアサート
※これ難しい...かも!? (解釈に "曖昧さ" あり、実際にデータが存在している方を優先)
生産販売可能な商品の購入を検索
  • 商品ステータス名称、退会理由テキスト (退会理由テーブル) も取得する(ログに出力) ※1
  • 購入価格の降順で並べる
  • 購入の紐づいている商品が生産販売可能であることをアサート
※1: ログについて、値がない項目は "none" を出力。if文使わないように。ヒント: Java8なら flatMap()
正式会員と退会会員の会員を検索
  • 会員ステータスの表示順で並べる
  • 会員が正式会員と退会会員であることをアサート
  • 両方とも存在していることをアサート
  • (検索されたデータに対して)Entity上だけで正式会員を退会会員に変更する
  • 変更した後、Entityが退会会員に変更されていることをアサート
  • 変更した後、データベース上は退会会員に変更されて "いない" ことをアサート ※1
※1: DBFluteは、Entityのデータを変更しても、updateをしない限りDBは不変である。(Thanks, nakano)
銀行振込で購入を支払ったことのある、会員ステータスごとに一番若い会員を検索
  • 正式会員で一番若い、仮会員で一番若い、という風にそれぞれのステータスで若い会員を検索
  • 一回の ConditionBean による検索で会員たちを検索すること (PartitionBy...)
  • ログのSQLを見て、検索が妥当であることを目視で確認すること
  • 検索結果が想定されるステータスの件数以上であることをアサート
  • ひとまず動作する実装ができたら、ArrangeQueryを活用してみましょう

区分値の追加と変更

実際に、区分値を追加して再自動生成してみましょう。DBFluteクライアント/playsql/data/common/xls/10-master.xls の MEMBER_STATUS にレコードを追加してください。

会員ステータスコード
HAN
会員ステータス名称
ハンズオン
説明
好きなこと書いて
表示順
9

そして、ReplaceSchema を再実行し、JDBCタスク、Docタスク、Generateタスクと連続で実行してください。 まずは、SchemaHTML の区分値一覧を確認し、追加ステータスが追加されていることを確認。 そして、テストメソッドを一つ作成して、追加したステータスを使って ConditionBean の条件を(適当に)組み立ててみてください。(JavaDocコメントに "説明" の内容が表示されていることも確認)

それができたら、今度はエクセルでそのレコードを削除して、同じように ReplaceSchemaからGenerateタスクまで実行して、 SchemaHTML の区分値一覧から消えて、かつ、そのテストメソッドがコンパイルエラーになることを確認してください。 (元の状態に戻ります)

そうしたら、そのテストメソッドはコメントアウトして、どういう確認を行ってどういう経緯でコメントアウトしたのかを説明するコメントを書いておいてください。 コメントアウトの理由を書くこと自体が大事なことです。 ちなみに、コメントアウトは該当行を選択して "ctrl + /"。

ネイティヴ型メソッドの削除

Java6,7 (DBFlute-1.0.x) では

区分値メソッドを生成しても、ネイティヴ型の設定メソッドはデフォルトの設定では残っています。 その理由はドキュメントに書いてある通りです。さっと目を通してみましょう。

その理由を割り切って、区分値メソッドでの設定の強制力を高めたい場合は、 DBFluteプロパティでネイティヴ型の設定メソッドを不可視(protected)にすることが可能です。 プロパティを設定して再自動生成し、ネイティヴ型の設定メソッドがいなくなったことを確認してみてください。

また、隠れ区分値を検索したときに明示的な例外になるような設定もしておきましょう。

Java8 (DBFlute-1.1) からは

ネイティヴ型の設定メソッドはデフォルトの設定で生成されないようになっています。

ですが、dfpropで区分値に定義していない "隠れ区分値" を検索してしまったりsetしてしまったりしても、デフォルトではエラーにはなりません。

littleAdjustmentMap.dfprop にて、classificationUndefinedHandlingType を EXCEPTION に設定すると、例外になるようになります。ぜひ設定をしてみましょう。

そもそも set できないから気軽に動作確認はできませんが、気合いのある人は "なにかしらの方法で" 無理やり protected になっている Member#setMemberStatusCode() を呼び出しても、止めはしません。

暗黙の区分値チェック

Java6,7 (DBFlute-1.0.x) では

暗黙の区分値は、データ登録時には何の制約もありません。 アプリ実装ではDBFluteの区分値メソッドを使ってタイプセーフにできますが、ReplaceSchemaのエクセルデータ作成ではケアレスミスが簡単に起きてしまいます。 DBFluteのオプションで登録時に自動チェックすることができます。

ハンズオンで追加した暗黙の区分値の中に、一つだけ "暗黙の区分値チェック" が付いていないものがありますので、付けてみて ReplaceSchema でチェックがされることを確認してみてください。

Java8 (DBFlute-1.1) からは

先ほどの classificationUndefinedHandlingType で既にチェックされるようになっています。そのままReplaceSchemaのエクセルデータを一時的に修正してチェックされることを確認してみましょう。

グルーピング判定

正式会員と仮会員で "サービスが利用できる会員" というグルーピングを設定をして再自動生成し、以下のエクササイズをやってみましょう。

サービスが利用できる会員を検索
  • グルーピングの設定によって生成されたメソッドを利用
  • 会員ステータスの表示順で並べる
  • 会員が "サービスが利用できる会員" であることをアサート

※SchemaHTMLの区分値の欄もぜひ確認してみてください。

※実は既にグルーピングを利用している区分値もありますので、ぜひ確認してみてください。

姉妹コードの利用

Flg区分値に true と false の姉妹コードを設定して再自動生成し、姉妹コードによって生成されたメソッドをうまく使って、以下のエクササイズをやってみましょう。

未払い購入のある会員を検索
  • 未払いの購入か支払済みの購入かを簡単に切り替えられるようにする
  • それを判断するprivateメソッドを作成して、戻り値のtrue/falseで切り替える
  • とりあえず未払いの購入を求められているので、そのメソッドの戻り値はfalse固定で
  • 姉妹コードの設定によって生成されたメソッドを利用
  • 正式会員日時の降順(nullを後に並べる)、会員IDの昇順で並べる
  • 会員が未払いの購入を持っていることをアサート
  • Assertでの検索が一回になるようにしてみましょう (LoadReferrer)

独自の属性を追加

会員ステータス区分値に、displayOrderという独自の属性(SubItem)を追加して再自動生成して、以下のエクササイズをやってみましょう。

会員ステータスの表示順カラムで会員を並べて検索
  • 会員ステータスの "表示順" カラムの昇順で並べる
  • 会員ステータスのデータ自体は要らない
  • その次には、会員の会員IDの降順で並べる
  • 会員ステータスのデータが取れていないことをアサート
  • 会員が会員ステータスの表示順ごとに並んでいることをアサート

次のセクション

さて、次のセクションへ