アプリ区分値の自動生成(appcls)

アプリ区分値とは?

アプリ区分値は、DBの区分値にて定義されない、とあるアプリプロジェクトの中だけで閉じる区分値です。 主には、ビュー側ロジックとアプリのロジックだけでやり取りして制御するものになります。

例えば、検索のソート順など、DBには保存されず、プログラムの中で使っておしまいのものです。 リストボックスで列挙されるものをイメージすると良いでしょう。 また、DBの区分値に近いものであっても、画面専用の要素が追加されていたり、要素が絞られていたり、何かしらDBの区分値をカスタマイズを加えているものも含まれます。

名前付き区分値との違いは

名前付き区分値は、業務ドメインを自由に定義することができます、アプリ区分値はあくまで、とあるアプリプロジェクト (maihama-docksideなど) という業務ドメインに特化した固定的な区分値であり、多少アバウトな名前で表現しています。

厳密には、すべての区分値を名前付きで業務ドメインを絞ったほうが良いかもしれませんが、あまり細かいと使い勝手も悪いので、とあるアプリプロジェクトというスコープの区分値をデフォルトで用意しています。 多くの場合は、DBの区分値とアプリの区分値で済むでしょう。そして、特別なドメインを意識すべき区分値だけ名前付き区分値にすると良いでしょう。

DB区分値
DB上のカラムで表現される区分値、DBFluteのもの
アプリ区分値 (appcls)
とあるアプリプロジェクトの中で閉じる区分値
名前付き区分値 (namedcls)
自由に定義できる特別なドメインを意識した区分値

LastaFluteではどうしてる?

LastaFluteでは、lastafluteMap.dfprop に appcls を指定して自動生成します。

DBFluteの区分値のクラス CDef と同じように、AppCDef というクラスが自動生成され、DBの区分値と同じような形で利用ができます。 Classificationインターフェースをimplementsしています。

また、DBの区分値との連携もできます。要素の定義をするときにDBの区分値を指定して、関連付けさせることができます。 そうすることで、定義の冗長化を防ぐとともに、DBFluteのCDefとの変換メソッドが付与されるなど、よりスムーズな実装が可能です。

アプリ区分値の作り方

まったくゼロの状態から作成する手順は以下の通りです。(1,2は最初の一回だけなので、二個目以降は3から)

  • 1. lastafluteMap.dfprop に appcls を追加
  • 2. [app]_appcls.dfprop を作成
  • 3. dfpropでアプリ区分値を定義
  • 4. FreeGenを叩いてCDefを自動生成
  • 5. 自動生成されたAppCDefをプログラムで利用

1. lastafluteMap.dfprop に appcls を追加

最初の一回だけです。lastafluteMap.dfprop の freeGenList に appcls を追加します。 (Exampleからスタートアップした場合は、大抵最初から定義されているので、多くの場合は不要なステップです)

e.g. Maihamaプロジェクトの lastafluteMap.dfprop @lastafluteMap.dfprop
map:{
    # your service name, camel case, initial uncapitalised
    ; serviceName = maihama

    # package for your domain name, e.g. com.example
    ; domainPackage = org.docksidestage

    # settings for common project of all web applications
    ; commonMap = map:{
        ; path = ..
        ; freeGenList = list:{ env ; config ; label ; message ; mail ; template ; doc ; namedcls }
        ; propertiesHtmlList = list:{ env ; config ; label ; message }
    }

    # settings for web applications
    ; appMap = map:{
        ; dockside = map:{
            ; path = ../../maihama-dockside
            ; freeGenList = list:{ env ; config ; label ; message ; mail ; template ; html ; doc ; appcls ; namedcls }
            ; propertiesHtmlList = list:{ env ; config ; label ; message }
        }
        ; hangar = map:{
            ; path = ../../maihama-hangar
            ; freeGenList = list:{ env ; config ; label ; message ; mail ; template ; doc ; appcls ; namedcls }
            ; propertiesHtmlList = list:{ env ; config ; label ; message }
        }
    }

    # you can override (several) default settings like this:
    #; overrideMap = map:{
    #    ; dockside.freeGen.mail.targetDir = ./playsql/data/mail
    #}
}

2. [app]_appcls.dfprop を作成

アプリプロジェクトごとに最初の一回だけです。src/main/resources配下に [app]_appcls.dfprop という名前のテキストファイルを作成します。

e.g. docksideプロジェクトでのアプリ区分値のdfpropの置き場所 @Directory
src/main/resources
 |-dockside_appcls.dfprop
 |-...
e.g. アプリ区分値のdfpropのひながた @[app]_appcls.dfprop
# /---------------------------------------------------------------------------
# appcls: (NotRequired - Default map:{})
#
# The definition of application classification.
#
# Specification:
# map: {
#     [classification-name] = list:{
#         ; map:{ topComment=[comment]; codeType=[String(default) or Number or Boolean] }
#         ; map:{ refCls=[projectName]@[DB classification name] ; refType=[included or exists or matches] }
#         ; map:{ code=[code]; name=[name]; alias=[alias]; comment=[comment] }
#     }
# }
#
# *The line that starts with '#' means comment-out.
#
map:{
}
# ----------------/

3. dfpropでアプリ区分値を定義

DBFluteのDB区分値の定義の仕方とほとんど同じです。

classificationDefinitionMap | DBFlute
e.g. アプリ区分値の定義、検索用の会員ステータス区分値 @[app]_appcls.dfprop
# /---------------------------------------------------------------------------
# appcls: (NotRequired - Default map:{})
#
# The definition of application classification.
#
# Specification:
# map: {
#     [classification-name] = list:{
#         ; map:{ topComment=[comment]; codeType=[String(default) or Number or Boolean] }
#         ; map:{ refCls=[projectName]@[DB classification name] ; refType=[included or exists or matches] }
#         ; map:{ code=[code]; name=[name]; alias=[alias]; comment=[comment] }
#     }
# }
#
# *The line that starts with '#' means comment-out.
#
map:{
    ; SearchMemberStatus = list:{
        ; map:{ topComment=MemberStatus for search condition ; codeType=String }
        ; map:{ code=FML; name=Formalized; alias=正式会員; comment=ちゃんとしてる }
        ; map:{ code=PRV; name=Provisinal; alias=仮会員; comment=ちゃんとしてない }
        ; map:{ code=WDL; name=Withdrawal; alias=退会会員; comment=ちゃんとできなかった }
        ; map:{ code=ALL; name=All; alias=すべて; comment=すべての会員ステータス }
    }
}
# ----------------/

アプリ区分値に関係のない機能 (isCheckImplicitSet, tableなど) は利用できません。

一方で、アプリ区分値固有の機能 (refClsなど) もあります。

4. FreeGenを叩いてCDefを自動生成

DBFlute の FreeGen (manage.sh 12) を叩きます。

すると、mylasta.appcls の下に AppCDef が自動生成されます。

5. 自動生成されたAppCDefをプログラムで利用

DBの区分値と同じように利用することができます。

Classificationインターフェースをimplementsしているので、DBのCDefで利用できるフレームワークの仕組みのほとんどが、同じように利用できます。

DB区分値との連携

DB区分値と連携をした定義ができます。連携することで、サービス全体での定義の冗長化を防いだり、DB側の変更への追従などのメリットがあります。

連携定義の仕方

refCls=[スキーマ名]@[DB区分値名] ; refType=[関連のルール]

という要素を追加することで、DBの区分値と連携ができます。

e.g. アプリ区分値のdfpropのひながた @[app]_appcls.dfprop
# /---------------------------------------------------------------------------
# appcls: (NotRequired - Default map:{})
#
# The definition of application classification.
#
# Specification:
# map: {
#     [classification-name] = list:{
#         ; map:{ topComment=[comment]; codeType=[String(default) or Number or Boolean] }
#         ; map:{ refCls=[projectName]@[DB classification name] ; refType=[included or exists or matches] }
#         ; map:{ code=[code]; name=[name]; alias=[alias]; comment=[comment] }
#     }
# }
#
# *The line that starts with '#' means comment-out.
#
map:{
    ; SearchMemberStatus = list:{
        ; map:{ topComment=MemberStatus for search condition ; codeType=String }
        ; map:{ refCls=maihamadb@MemberStatus ; refType=included }
        ; map:{ code=ALL; name=All; alias=All Statuses; comment=without status filter }
    }
}
# ----------------/

参照区分値(refCls)の設定

refClsで、関連するDBの区分値を指定します。

スキーマ名
そのDB区分値が所属しているDBFluteのクライアント名
区分値名
そのDB区分値の名前 (classificationDefinitionMap.dfpropに定義されている名前)

DBFlute-1.2.5 以降

appclsなら、(DBではなく)同アプリ内のnamedcls区分値を参照できます。(@since DBFlute-1.2.5)

その場合、スキーマ名が namedcls の名前になります。 例えば、namedclsのdfpropファイル名が [app]_leonardo_cls.dfprop であれば、スキーマ名は leonardo_cls になります。

(namedcls から namedcls を参照する場合は、FreeGenでの評価順序に依存します)

関連のルール(refType)の設定

どういう関連なのかを指定します。

included
そのDB区分値の要素をまるごと持ってくる (定義の再利用)
exists
アプリ区分値の要素が、そのDB区分値に含まれている (定義のチェック)
matches
アプリ区分値の要素セットが、そのDB区分値の要素セットと完全一致 (定義のチェック)

includedしつつ一部属性を上書き (@since DBFlute-1.2.5)

includedしつつ、区分値要素の一部属性を上書き(override)できます。

e.g. refClsでincludedしつつ一部属性をオーバーライド @dfprop
# maihamadb@MemberStatus includes { FML, WDL, PRV }
# overriding FML's name attribute by the literal element
# overriding PRV's alias, sisterCode, subItemMap attributes by the literal element
; AppDohotel = list:{
    ; map:{ topComment=test of included with overriding, expected merged ; codeType=String }
    ; map:{ refCls=maihamadb@MemberStatus ; refType=included }
    ; map:{ code=FML ; override=true ; name=OneMan }
    ; map:{ code=PRV ; override=true ; alias=Castle ; sisterCode=Route ; subItemMap=map:{order=9} }
    ; map:{ code=MYS ; name=Mystic ; alias=Hangar ; comment=Rhythms ; subItemMap=map:{order=88} }
}
  • maihamadb@MemberStatusで、FML, WDL, PRV が include される
  • includeされたFMLのname属性だけが上書きされる (aliasなどはそのまま)
  • includeされたWDLは特に上書きする属性はないのでincludedされたそのままの属性値になる
  • includeされたPRVのalias,sisterCode,subItemMap属性が上書きされる (nameはそのまま)
  • override属性なしで同じコードを定義した場合は自動生成時にエラー (この例ではそのケースはない)
  • MYSは普通に追加の要素として登録される

existsしつつ一部属性を継承 (@since DBFlute-1.2.5)

exists/matchesしつつ、区分値要素の一部属性を参照先から継承(inherit)できます。

e.g. refClsでexistsチェックしつつ一部属性を継承 @dfprop
# maihamadb@MemberStatus links { FML, WDL, PRV }
# inheriting FML's all attributes by the literal element
# inheriting PRV's name, sisterCode, subItemMap attributes by the literal element
; AppAmphi = list:{
    ; map:{ topComment=test of included with overriding, expected merged ; codeType=String }
    ; map:{ refCls=maihamadb@MemberStatus ; refType=exists }
    ; map:{ code=FML ; inherit=true }
    ; map:{ code=PRV ; inherit=true ; alias=Castle ; comment=ParadeTwoYears }
}
  • maihamadb@MemberStatusで、FML, WDL, PRV が existsチェックの期待値になる
  • 直接記述したFMLはすべての属性がMemberStatusから継承される (オーソドックスなパターン)
  • 直接記述したPRVはname,comment属性は記述されたそのままに、alias属性やsisterCodeなどが継承される
  • inherit属性なしで同じコードを定義した場合は、直接記述したものだけが属性になる
  • refType=matchesでも全く同じように利用できる

refTypeの使い分け (@since DBFlute-1.2.5)

上書きや継承ができるようになったことで、refTypeの使い分けが以下のようになります。

要素セットが増える
refType=included, カスタマイズ属性はoverride, 追加要素は直接記述
要素セットが減る
refType=exists, 基本inheritでカスタマイズ属性だけ定義
要素セットが同じ
refType=matches 基本inheritでカスタマイズ属性だけ定義

まず、どのrefTypeでもカスタマイズ属性を定義できるので、それでどのrefTypeを使うか?を気にする必要はほとんどありません。 (カスタマイズ属性が多いか少ないかで、やりやすさの違いはあるかもですが)

ゆえに、アプリ区分値で定義したい要素セットが参照先の区分値に対して増える?減る?同じ?で考えても良いでしょう。

一方で、"要素セットが減りつつも独自要素が増える" ような場合は、どの refType でも表現ができません。 (そのときは、ぜひ要望を出してください。必要性があるのかわからないので、まだ実装していません)

DBFlute-1.2.4 までの使い分け (@until DBFlute-1.2.4)

通常は、included を使うことが多いでしょう。DB区分値の要素に加えて、固有の要素を追加するような場合などを想定しています。 (全く同じなのであれば、そもそもアプリ区分値を作る必要性がないので)

DB区分値と関連はするものの、すべての要素を使うわけじゃなかったり、name や alias などの定義を独自のものにしたい場合は、exists になります。 存在しない要素を定義した場合は、自動生成時点で落ちます。DB変更があったときに気づくことができます。

すべての要素を使うけれども、定義を独自のものにしたい場合は、matches になります。

DBのCDefとの変換

自動生成されたアプリ区分値の AppCDef に、DB の CDef との変換を司るメソッドが生成されます。

static fromDBCls(dbCls)
引数で指定されたDBのCDefからAppCDefを探す
toDBCls()
いまのAppCDefの区分値要素に対応するDBのCDefの要素を探す

※namedcls参照の場合は、メソッド名の DB が Ref に変わります。(@since DBFlute-1.2.5)

Thymeleafとの連携

Thymeleafを使っている場合、LastaThymeleaf の optionCls で、アプリ区分値をそのまま利用したいものです。

optionCls属性は、LastaFlute の ListedClassificationProvider インターフェース経由で、区分値の情報を取得します。なので、[App]ListedClassificationProvider クラスで、アプリ区分値の制御を入れれば利用できるようになります。

e.g. fortressプロジェクトのListedClassificationProvider (mylasta.sponsor) @Java
public class FortressListedClassificationProvider extends TypicalListedClassificationProvider {

    @Override
    protected Function<String, ClassificationMeta> chooseClassificationFinder(String projectName)
            throws ProvidedClassificationNotFoundException {
        if (DBCurrent.getInstance().projectName().equals(projectName)) {
            return clsName -> onMainSchema(clsName).orElse(null); // null means not found
        } else {
            throw new ProvidedClassificationNotFoundException("Unknown DBFlute project name: " + projectName);
        }
    }

    @Override
    protected Function<String, ClassificationMeta> getDefaultClassificationFinder() {
        return clsName -> {
            return onMainSchema(clsName).orElseGet(() ->{
                return onAppCls(clsName).orElse(null); // null means not found
            });
        };
    }

    protected OptionalThing<ClassificationMeta> onMainSchema(String clsName) {
        return findMeta(CDef.DefMeta.class, clsName);
    }

    protected OptionalThing<ClassificationMeta> onAppCls(String clsName) {
        return findMeta(AppCDef.DefMeta.class, clsName);
    }
}