業務的one-to-one

業務的one-to-oneとは?

構造的には one-to-many でも、(業務的な)固定の条件を加えることによって one-to-one として扱うことのできるリレーションのことです。biz-one-to-one(びずわんとぅわん)とも表現します。

例えば、現在の住所

ExampleDBでは、会員と会員住所情報の関係が該当します。

e.g. 会員テーブルと会員住所テーブルのデータのExample (2014/10/14時点) @Java
| 会員          | | 会員住所                       |
| 1: Stojkovic | | 茂原: 2004-11-02 - 2007-01-13 |
| 1: Stojkovic | | 市原: 2007-01-14 - 2011-07-21 |
| 1: Stojkovic | | 長柄: 2011-07-21 - 2016-07-31 | // ここ現在
| 1: Stojkovic | | 鴨川: 2016-08-01 - 9999-12-31 | // 引っ越し予定
| 2: Savicevic | | 東金: 2002-02-22 - 2011-11-16 |
| 2: Savicevic | | 館山: 2011-11-17 - 9999-12-31 | // ここ現在
| ...          | | ...                           |

物理的には one-to-many でも、業務的には会員と現在の会員住所でone-to-one として扱う、というようなパターンのカージナリティを 業務的one-to-one と呼んでいます。

有効期間を指定することで、現在住所という概念のビューと結合することができます。

e.g. 業務的one-to-oneの検索SQL {MEMBER, MEMBER_ADDRESS} @Java
select MEMBER.*
     , MEMBER_ADDRESS.* -- ONE member has ONE address at the date
  from MEMBER
    left outer join MEMBER_ADDRESS
      on MEMBER.MEMBER_ID = MEMBER_ADDRESS.MEMBER_ID
     and MEMBER_ADDRESS.VALID_BEGIN_DATE <= '2014-10-14'
     and MEMBER_ADDRESS.VALID_END_DATE >= '2014-10-14'

間違えそうだし、変更もつらい

結合条件で書けば済みますが、バグの温床になりやすいところでもあります。 複数の箇所でこれを書くことになるので、誰か一人くらい比較条件のイコール(=)が抜けたり、条件が片方落ちたりってやりそうです。 さらに、つらいところが、テストデータの精度によってはそれなりにうまく動いてしまうので、バグの発見に時間がかかってしまうところです。

また、DB変更でその固定条件が変わったとき、この固定条件を複数の箇所で書かれていると、その影響範囲の特定が至難の業となります。 もともと、staticに決まったものでありますから、その固定条件自体を一元管理したいものです。

自動解決はできない

このリレーションは、一般にリレーショナルデータベースの仕組みとしては表現されないもので(制約を掛けることはできない)、 固定の条件を加えることでそのようなリレーションになるというのは人(ドキュメント)しか知りません。 よって、自動生成ツールなどでこのリレーションを利用した処理を自動生成しようと思っても判断ができないため、 (アプリケーションとしては)静的な構造であるにも関わらず、ディベロッパーはこの join 句への固定条件を常に意識して実装する必要があります。

リレーションを定義して定型利用

そういった特徴から、DBFluteでもデフォルトではこの業務的one-to-oneに対して何もアプローチできませんが、 静的な構造であることに変わりはないため、その構造の情報をプロパティにて定義することで、通常のリレーションと同じように扱えるようにします。 一度設定してしまえば、アプリではこの join 句での(業務的な)固定条件を意識せずにリレーションを利用できるようになり、 ディベロッパーへの統一した実装、かつ、安全性の提供 にもなります。(で通常のリレーションと同じように扱える)

(プロパティで設定して再自動生成すると)以下のような実装ができるようになり、実行すると join 句には指定された固定条件が自動的に付与され、one-to-oneとして扱うことができるようになります。

e.g. 業務的one-to-oneを利用したConditionBeanの検索 {MEMBER, MEMBER_ADDRESS} @Java
Date targetDate = ...;
MemberCB cb = new MemberCB();
cb.setupSelect_MemberAddressAsValid(targetDate);
List<Member> memberList = memberBhv.selectList(cb);
for (Member member : memberList) {
    MemberAddress address = member.getMemberAddressAsValid();
}

業務的one-to-oneの設定(dfprop)

additionalForeignKeyMap.dfprop を利用して、リレーションを定義すると共に、固定条件 fixedCondition を指定することで、業務的one-to-oneのリレーションを定義することができます。

e.g. 会員から会員住所情報への有効期間条件付きのFK @additionalForeignKeyMap.dfprop
...
    ; FK_MEMBER_MEMBER_ADDRESS_AS_VALID = map:{
        ; localTableName  = MEMBER    ; foreignTableName  = MEMBER_ADDRESS
        ; localColumnName = MEMBER_ID ; foreignColumnName = MEMBER_ID
        ; fixedCondition =
         $$foreignAlias$$.VALID_BEGIN_DATE <= /*targetDate(Date)*/null
     and $$foreignAlias$$.VALID_END_DATE >= /*targetDate(Date)*/null
        ; fixedSuffix = AsValid
        ; comment = relation of valid address for the specified target date 
    }
...

fixedConditionのインデントがそのままSQLに反映されます。 二つ目の条件の and を上記のExampleコードの位置で書けば(半角空白が5つ)、SQL上で綺麗に並びます。

commentの内容は、JavaDocに反映されます。 必須ではありませんが、業務的な一行コメントを入れておくとディベロッパーの迷いも消えるでしょう。

区分値を利用した固定パラメーター

固定条件に固定のパラメーターを利用する場合、そのまま値をハードコードすることもできますが、それがもし区分値であるならば 埋め込み区分値コメント を使って区分値参照ができます。これを使えば、間違った区分値を使ってしまう恐れがなくなります。

e.g. 埋め込み区分値コメントを利用、正式会員 @additionalForeignKeyMap.dfprop
...
    ; FK_MEMBER_MEMBER_ADDRESS_AS_VALID = map:{
        ...
        ; fixedCondition =
 $$localAlias$$.MEMBER_STATUS_CODE = /*$cls(MemberStatus.Formalized)*/null
        ...
    }
...

引数指定の動的パラメーター

固定条件に動的なパラメーター(引数)が必要な場合は、バインド変数コメント が利用できます。ConditionBean のリレーション関連のメソッドにおいて、メソッドの引数にて条件値を指定できるようになります(指定する必要があります)。

e.g. バインド変数コメントを利用、targetDateという名前のDate型 @additionalForeignKeyMap.dfprop
...
    ; FK_MEMBER_MEMBER_ADDRESS_AS_VALID = map:{
        ...
        ; fixedCondition =
         $$foreignAlias$$.VALID_BEGIN_DATE <= /*targetDate(Date)*/null
     and $$foreignAlias$$.VALID_END_DATE >= /*targetDate(Date)*/null
        ...
    }
...
e.g. バインド変数を指定する SetupSelect {MEMBER, MEMBER_ADDRESS} @Java
Date targetDate = ...;
MemberCB cb = new MemberCB();
cb.setupSelect_MemberAddressAsValid(targetDate);

パラメーターの型で、CDefを利用することもできます。

e.g. memberStatus という変数名でCDef.MemberStatus型のバインド変数 @additionalForeignKeyMap.dfprop
$$foreignAlias$$.MEMBER_STATUS_CODE <= /*memberStatus($$CDef$$.MemberStatus)*/null

一方通行のリレーション

この業務的one-to-oneは、一方通行のリレーションであり、ここで設定した one-to-one としての逆参照のメソッドは自動生成されません(会員住所情報から会員への参照)。通常、one-to-manyの関係のあるテーブル間で利用することが想定されるため、 逆参照のメソッドは(one-to-manyとして)既にある、という状態なので実装上の支障はありません。

設定のタイミング

このリレーションは、DB設計が固まった時点(DBFluteで自動生成ができる時点)で、どこに業務的one-to-oneが存在するのか洗い出すことができます。 また、DB設計の成果として少なくともドキュメント上に洗い出されてなければなりません。 つまり、このリレーションの(DBFluteプロパティにおける)定義は最初のDBFluteのセットアップ直後のタイミングで設定することができるため、 実装時に必要になった時にではなく、最初から設定された状態でディベロッパーに横展開 することをお奨めします。 (RDBとして未解決なデータ構造をディベロッパーに未解決のまま渡すではなく、少なくともDBFluteとして解決されている状態で横展開)

動的パラメーター利用時の実装

SetupSelect と Query

パラメーターがあるとき、SetupSelect と Query を両方利用する場合は、必ず両方に同じ値を指定します。 もし、違う値を指定してしまった場合は、最後に設定された値が有効になります(内部的には上書きをしているだけ)。

e.g. パラメーターを指定する SetupSelect と Query {MEMBER, MEMBER_ADDRESS} @Java
Date targetDate = ...;
MemberCB cb = new MemberCB();
cb.setupSelect_MemberAddressAsValid(targetDate);
cb.query().queryMemberAddressAsValid(targetDate).setAddress_...

そのような仕様のため、一つのリレーションで "2007/01/01の会員住所" と "2008/01/01の会員住所" を同時に取り扱うことはできません。そのような場合は、fixedSuffix だけを変えた別のリレーションを(業務的に意味のある名前で)定義することをお奨めします。

SpecifyColumn

パラメーターがあるとき、SpecifyColumn のリレーションを辿る Specify(Relation) では既に SetupSelect で条件値が指定されていることが前提となるため、条件値の指定は不要です(そもそも引数が存在しない)。

e.g. パラメーターを利用したリレーションの SpecifyColumn {MEMBER, MEMBER_ADDRESS} @Java
Date targetDate = ...;
MemberCB cb = new MemberCB();
cb.setupSelect_MemberAddressAsValid(targetDate);
cb.specify().specifyMemberAddressAsValid().columnAddress();

一方で、パラメーター指定(引数)ありの Specify(Relation) のメソッドも存在します(@since 1.0.4F)。 ここで SetupSelect と同じ値を入れても特に問題はありません。(こちらのメソッドは、ColumnQuery や DerivedReferrer などのカラム指定で利用するときのためのメソッドです)

e.g. パラメーターを指定したリレーションの Specify(Relation) {MEMBER, MEMBER_ADDRESS} @Java
Date targetDate = ...;
MemberCB cb = new MemberCB();
cb.setupSelect_MemberAddressAsValid(targetDate);
cb.specify().specifyMemberAddressAsValid(targetDate).columnAddress();

ColumnQuery

パラメーターがあるとき、ColumnQuery では、パラメーター指定ありの Specify(Relation) を利用します。@since 1.0.4F

e.g. ColumnQueryでパラメーターを指定したリレーションの Specify(Relation)  {MEMBER, MEMBER_ADDRESS} @Java
final Date targetDate = ...;
MemberCB cb = new MemberCB();
cb.columnQuery(new SpecifyQuery<MemberCB>() {
    public void specify(MemberCB cb) {
        cb.specify().columnBirthdate();
    }
}).lessThan(new SpecifyQuery<MemberCB>() {
    public void specify(MemberCB cb) {
        cb.specify().specifyMemberAddressAsValid(targetDate).columnValidBeginDate();
    }
});

パラメーターを指定できる Specify(Relation) のメソッドがないDBFluteバージョン(@until 1.0.4D)の場合は、外側で Query(Relation) を使ってパラメーターを指定して補完します。

e.g. ColumnQueryでパラメーターなし Specify(Relation) を外側で補完  {MEMBER, MEMBER_ADDRESS} @Java
Date targetDate = ...;
MemberCB cb = new MemberCB();
cb.query().queryMemberAddressAsValid(targetDate);
cb.columnQuery(new SpecifyQuery<MemberCB>() {
    public void specify(MemberCB cb) {
        cb.specify().columnBirthdate();
    }
}).lessThan(new SpecifyQuery<MemberCB>() {
    public void specify(MemberCB cb) {
        cb.specify().specifyMemberAddressAsValid().columnValidBeginDate();
    }
});

DrivedReferrer

パラメーターがあるとき、(Specify)DrivedReferrer や (Query)DrivedReferrer では、パラメーター指定ありの Specify(Relation) を利用します。@since 1.0.4F

e.g. (Specify)DrivedReferrer でパラメーターを指定したリレーションの Specify(Relation) @Java
final Date targetDate = ...;
MemberStatusCB cb = new MemberStatusCB();
cb.specify().derivedMemberList().max(new SubQuery<MemberCB>() {
    public void query(MemberCB subCB) {
        subCB.specify().specifyMemberAddressAsValid(targetDate).columnValidBeginDate();
    }
}, MemberStatus.ALIAS_...);

パラメーターを指定できる Specify(Relation) のメソッドがないDBFluteバージョン(@until 1.0.4D)の場合は、Query(Relation) を使ってパラメーターを指定して補完します。

e.g. (Specify)DrivedReferrer でパラメーターなし Specify(Relation) @Java
final Date targetDate = ...;
MemberStatusCB cb = new MemberStatusCB();
cb.specify().derivedMemberList().max(new SubQuery<MemberCB>() {
    public void query(MemberCB subCB) {
        subCB.specify().specifyMemberAddressAsValid().columnValidBeginDate();
        subCB.query().queryMemberAddressAsValid(targetDate);
    }
}, MemberStatus.ALIAS_...);

別リレーションのカラム利用

OverRelation

該当リレーションとは別のリレーションを経由したテーブルのカラムを fixedCondition の条件で利用することができます。これを FixedCondition の OverRelation と呼びます。@since 0.9.7.5

例えば、会員と会員住所情報の業務的one-to-oneにおける fixedCondition の中で、会員ステータスのカラムや会員退会情報のカラムを利用することができます。 主に有効日時を利用した場合において、プログラムから与える一律の日時(現在日時など)ではなく、別リレーションのテーブルで保持する日時を利用する場合に有効です。

fixedCondition の定義の中で、alias 名変数として、$$over([テーブル名表現].[リレーション名])$$ と記述することで、該当するリレーションを探して alias 名として自動解決します。

テーブル名
参照元テーブルと利用カラムのテーブルを両方参照できるポイントとなるテーブル(ポイントテーブル)。
  • A. 該当の業務的one-to-oneの参照元テーブル(Local)
  • B. 該当の業務的one-to-oneの参照先テーブル(Foreign)
  • C. 参照元テーブルを参照するテーブル(Referrer)
  • D. 参照元テーブルを参照するテーブルを参照するテーブル(無限階層のReferrer)
"A" の場合は固定で $localTable、"B" の場合は $foreignTable と指定する(それぞれ決められたワードの前に "$" マーク)。それ以外の場合(Refererr)はテーブル名そのままを指定する。
リレーション名
指定されたテーブルが参照する、条件に利用したいカラムを持つテーブル(Foreign)へのリレーション。 リレーション名はEntityのプロパティ名に相当し、"." (ドット)区切りで無限階層にリレーションを辿ることができる。また、SetupSelect や Query されていないリレーションは、自動で Query(Relation) される。
※(別の)業務的one-to-oneのリレーションを辿ることはできない(機能制限)
※利用したいカラムを持っているテーブルが "C" の場合は省略可能(指定の必要がないため:後述)

LocalテーブルのForeignテーブル

例えば、会員と会員住所情報の業務的one-to-oneの固定条件に、会員退会情報の退会日時を利用したい場合は、テーブル名は $localTable、リレーション名は memberWithdrawalAsOne と指定します。

e.g. 参照元テーブルの Foreign テーブルのカラムを利用 @additionalForeignKeyMap.dfprop
...
    ; FK_MEMBER_MEMBER_ADDRESS_VALID = map:{
        ; localTableName  = MEMBER    ; foreignTableName  = MEMBER_ADDRESS
        ; localColumnName = MEMBER_ID ; foreignColumnName = MEMBER_ID
        ; fixedCondition =
         ...
     and $$foreignAlias$$.VALID_BEGIN_DATE
       >= $$over($localTable.memberWithdrawalAsOne)$$.WITHDRAWAL_DATETIME
        ; fixedSuffix = AsValid
        ; comment = ...
    }
...

ForeignテーブルのForeignテーブル

例えば、会員と会員ログイン情報との関係で、会員ログイン情報経由の会員ステータスの表示順が 2 のときに業務的one-to-oneになるとしたら、テーブル名は $foreignTable、リレーション名は memberStatus と指定します。SQLでは、Foreignテーブルがインラインビューになり、その中で結合をして該当カラムを利用できるようにします。ゆえに、解決される alias 名は、Foreignテーブルと同じものになります。

e.g. ForeignテーブルのForeignテーブルのカラムを利用 @additionalForeignKeyMap.dfprop
...
    ; FK_MEMBER_MEMBER_LOGIN_VALID = map:{
        ; localTableName  = MEMBER    ; foreignTableName  = MEMBER_LOGIN
        ; localColumnName = MEMBER_ID ; foreignColumnName = MEMBER_ID
        ; fixedCondition =
     $$over($foreignTable.memberStatus)$$.DISPLAY_ORDER = 2
        ; fixedSuffix = AsValid
        ; comment = ...
    }
...
e.g. ForeignテーブルのForeignテーブルのカラムを利用する時のSQL @DisplaySql
select ...
  from MEMBER dfloc
    left outer join (select dffixedbase.*, dffixedjoin_0_0.DISPLAY_ORDER
                       from MEMBER_LOGIN dffixedbase
                         left outer join MEMBER_STATUS dffixedjoin_0_0 ...
                    ) dfrel_4
      on dfloc.MEMBER_ID = dfrel_4.MEMBER_ID
     and dfrel_4.DISPLAY_ORDER = 2

結合条件の中で、インラインビューの中に含めることができるとDBFluteに自動判別されたものは、インラインビューに展開されます。 これにより、ビューマージが発生しないDBMSにおけるパフォーマンス劣化が予防されます。 @since 1.0.5A

e.g. ForeignテーブルのForeignテーブルの自動判別によるインライン展開 @DisplaySql
select ...
  from MEMBER dfloc
    left outer join (select dffixedbase.*, dffixedjoin_0_0.DISPLAY_ORDER
                       from MEMBER_LOGIN dffixedbase
                         left outer join MEMBER_STATUS dffixedjoin_0_0 ...
                      where dffixedjoin_0_0.DISPLAY_ORDER = 2
                    ) dfrel_4
      on dfloc.MEMBER_ID = dfrel_4.MEMBER_ID

Foreignテーブルのカラムの別名

もし、Foreignテーブルのカラムの中に該当のカラムと同名のものが存在する場合は、名前が曖昧になるので、 そのときは over() の第二引数に該当のカラム名を指定することで、alias 名変数の後に続くカラムを任意の alias 名で指定できるようになります。インラインビューの中では select 句で as を使って alias 名が宣言されます。

e.g. ForeignテーブルのForeignテーブルのカラムを alias 名で扱う @additionalForeignKeyMap.dfprop
...
    ; FK_MEMBER_MEMBER_LOGIN_VALID = map:{
        ; localTableName  = MEMBER    ; foreignTableName  = MEMBER_LOGIN
        ; localColumnName = MEMBER_ID ; foreignColumnName = MEMBER_ID
        ; fixedCondition =
$$over($localTable.memberStatus)$$.DISPLAY_ORDER
    = $$over($foreignTable.memberStatus, DISPLAY_ORDER)$$.STATUS_ORDER
        ; fixedSuffix = AsValid
        ; comment = ...
    }
...
e.g. 参照先テーブルの Foreign テーブルのカラムを alias 名で扱うときのSQL @DisplaySql
select ...
  from MEMBER dfloc
    left outer join (select ..., ...DISPLAY_ORDER as STATUS_ORDER
                       from MEMBER_LOGIN dffixedbase
                         left outer join MEMBER_STATUS dffixedjoin_0_0 ...
                    ) dfrel_4
      on dfloc.MEMBER_ID = dfrel_4.MEMBER_ID
     and dfrel_0.DISPLAY_ORDER = dfrel_4.STATUS_ORDER

基点テーブル限定パターン

指定されたテーブルが、Localテーブルではなく、そのテーブルを参照したテーブル(Referrer)、 もしくは、そのテーブル(Referrer)を参照するテーブルである場合は、該当の業務的one-to-oneのリレーションを利用するときの ConditionBean の基点テーブルは、その Referrer テーブル自身、もしくは、そのテーブルを参照するさらなる Referrer テーブル(無限階層)に限られます。 (もし、そうでないテーブルを基点にしてその業務的one-to-oneを利用すると、エリアス名が解決できないためSQL上でエラーになります)

Localテーブルを参照したテーブル自体(Referrer)のカラムを利用する場合は、リレーション名を省略します。 (省略記法は、この場合のみ利用可能です)

e.g. Referrer テーブルのカラムを利用 (基点は PURCHASE 前提)  @additionalForeignKeyMap.dfprop
...
    ; FK_MEMBER_MEMBER_ADDRESS_VALID = map:{
        ; localTableName  = MEMBER    ; foreignTableName  = MEMBER_ADDRESS
        ; localColumnName = MEMBER_ID ; foreignColumnName = MEMBER_ID
        ; fixedCondition =
     ...END_DATE >= $$over(PURCHASE)$$.PURCHASE_DATETIME
        ; fixedSuffix = AsValid
        ; comment = ...
    }
...
e.g. Referrer の Foreign テーブルのカラムを利用 (基点は PURCHASE 前提)  @additionalForeignKeyMap.dfprop
...
    ; FK_MEMBER_MEMBER_ADDRESS_VALID = map:{
        ; localTableName  = MEMBER    ; foreignTableName  = MEMBER_ADDRESS
        ; localColumnName = MEMBER_ID ; foreignColumnName = MEMBER_ID
        ; fixedCondition =
     ...END_DATE >= $$over(PURCHASE.product.productStatus)$$.UPDATE_DATETIME
        ; fixedSuffix = AsValid
        ; comment = ...
    }
...

一つの業務的one-to-oneに対して、(OverRelationでない)バインド変数利用と基点テーブル限定パターンを両方使う必要がある場合は、fixedSuffix を変えて、それぞれ専用のリレーションにすると良いでしょう。例えば、バインド変数利用は(お約束通りの) AsValid で、基点テーブル限定パターンは By[カラム名表現] など。

結合の自動解決

指定されたリレーションが SetupSelect や Query で指定されていない場合は、そのリレーションまで自動的に Query(Relation) されます。(SQL上の join 句は自動で解決されます)

リレーション名のきまぐれ

リレーション名は、同じテーブルに対して複数FKになる場合に、単なるテーブル名のキャメルケースではなくなりますのでご注意下さい(特に後から二つ目のFKが追加された場合)。 例えば、会員が複数の会員ステータス参照のカラムを持っていた場合は、それぞれ memberStatusByFooCode、memberStatusByBarCode というように By[FKカラム] が後ろに付与されます。

導出的one-to-one

導出的one-to-oneって?

何かしら導出した値に一致するレコード(導出レコード)に対する業務的one-to-one(導出的one-to-one)を利用することもできます。 例えば、会員を検索する際、同時に会員の最終ログインの情報も取得したい場合など。(会員IDとログイン日時でユニークであることが前提)

fixedConditionで相関サブクエリ

最終ログイン日時だけが欲しいなら (Specify)DerivedReferrer で取得できますが、最終ログインのレコードがまるごと欲しい場合は、それでは取得することはできません。 "最終ログインのレコード" に対して業務的one-to-oneのリレーションを設定して SetupSelect(Relation) する必要があります。 そのリレーションは、固定条件(fixedCondition)で相関サブクエリを利用することで実現できます。

e.g. 最終ログインのレコードに対する業務的one-to-one(導出的one-to-one) @additionalForeignKeyMap.dfprop
...
    ; FK_MEMBER_MEMBER_LOGING_LATEST = map:{
        ; localTableName  = MEMBER    ; foreignTableName  = MEMBER_LOGIN
        ; localColumnName = MEMBER_ID ; foreignColumnName = MEMBER_ID
        ; fixedCondition =
$$foreignAlias$$.LOGIN_DATETIME = ($$sqbegin$$
select max(login.LOGIN_DATETIME)
  from MEMBER_LOGIN login
 where login.MEMBER_ID = $$foreignAlias$$.MEMBER_ID
)$$sqend$$
        ; fixedSuffix = AsLatest
        ; comment = ...
    }
...

このようにすると、SQLでは left outer join の on 句の条件の中に相関サブクエリが展開され、最終ログインのレコードを取得できます。

インラインビューで実現することもできる

ただし、このSQLが文法上許されない DBMS も存在します(例えば、Oracle とか PostgreSQL など)。 その場合は、この相関サブクエリの固定条件がインラインビューに展開されるように設定することで回避できます。 fixedInline を true にすることでインラインビューに展開されます。

e.g. 最終ログインのレコードに対する業務的one-to-one(導出的one-to-one) @additionalForeignKeyMap.dfprop
...
    ; FK_MEMBER_MEMBER_LOGING_LATEST = map:{
        ; localTableName  = MEMBER    ; foreignTableName  = MEMBER_LOGIN
        ; localColumnName = MEMBER_ID ; foreignColumnName = MEMBER_ID
        ; fixedCondition =
$$foreignAlias$$.LOGIN_DATETIME = ($$sqbegin$$
select max(login.LOGIN_DATETIME)
  from MEMBER_LOGIN login
 where login.MEMBER_ID = $$foreignAlias$$.MEMBER_ID
)$$sqend$$
        ; fixedSuffix = AsLatest
        ; fixedInline = true
        ; comment = ...
    }
...

インラインビューでも文法上許されないDBMSの場合は、この機能は利用できません。

業務的many-to-one

業務的many-to-oneって?

業務的one-to-oneではなく、業務的many-to-oneです。とある条件を入れることによって many-to-one の関係になるようなリレーションを示します。本来、複数のテーブルに分かれるべき情報が一つのテーブルにまとまって入っているような構造で発生しやすいです。

e.g. 業務的many-to-oneのデータ @Model
PK_ID, GENERAL_ID, TARGET_TYPE
 + = = = = = = = = = +
 |  1  |  1  |  SEA  |
 |  2  |  1  |  SEA  |
 |  3  |  2  |  SEA  |
 |  4  |  3  |  SEA  |
 | - - - - - - - - - |
 |  5  |  1  |  LND  |
 |  6  |  2  |  LND  |
 + = = = = = = = = = +
  • PK_ID {1,2,3,4} の GENERAL_ID {1,1,2,3} は、SEA_IDを示す
  • PK_ID {5,6} の GENERAL_ID {1,2} は、LAND_IDを示す
  • TARGET_TYPE次第で、そのIDの指し示すテーブルが変わるのが特徴 (FK制約は貼れない)

fixedConditionでmany-to-oneに

業務的one-to-one比べてあまり発生頻度は多くないと想定されますが、いざ必要になったときは、 業務的one-to-oneと同様に、fixedConditionを使ってリレーションを作り上げることができます。 (ExampleDBで合致するテーブルがないので、FOOとBARを例にします)

e.g. 自テーブルの種別を固定にして業務的many-to-one @additionalForeignKeyMap.dfprop
...
    ; FK_FOO_BAR = map:{
        ; localTableName  = FOO    ; foreignTableName  = BAR
        ; localColumnName = GENERAL_ID ; foreignColumnName = BAR_ID
        ; fixedCondition = $$localAlias$$.FOO_TYPE = /*$cls(FooType.Bar)*/null
        ; fixedSuffix = AsBar
        ; comment = ...
    }
...

ただしこの場合、FOOからBARへの関連は作成されますが、BARからFOOへの逆参照は作成されません。 業務的one-to-oneの場合はそれ自体が逆参照と言えるので(FK参照の逆として)、BARからFOOへの逆参照は必要ありませんでしたが、 業務的many-to-oneはそれ自体がFK参照の代わりとなるので、BARからFOOを many として扱える逆参照が欲しくなります。その場合は、fixedReferrerプロパティをtrueにすることで逆参照が作成されます@since 0.9.9.7A

e.g. 関連テーブルの種別を固定にして業務的many-to-one @additionalForeignKeyMap.dfprop
...
    ; FK_FOO_BAR = map:{
        ; localTableName  = FOO    ; foreignTableName  = BAR
        ; localColumnName = GENERAL_ID ; foreignColumnName = BAR_ID
        ; fixedCondition = $$localAlias$$.FOO_TYPE = /*$cls(BarType.Foo)*/null
        ; fixedSuffix = AsBar
        ; fixedReferrer = true
        ; comment = ...
    }
...

このようにすることで、BAR側で ExistsReferrer や LoadReferrer などが利用できます。 相関条件には固定条件(fixedCondition)が付与されます。

SchemaHTMLでの表示

Docタスクで自動生成される SchemaHTML では、設定された業務的one-to-oneのリレーションも表示されます。 他のFK制約によるリレーションとは見た目の表現が少し違い、区別が付くようになっています。

Exampleのススメ

dbflute-basic-example では、実際に会員と会員住所情報の業務的one-to-oneを設定して、テストケースの中で利用しています。 (他のほとんどのExampleでも同様に利用されています)

講演会でのスライド資料

参考までに。