LoadReferrer

概要

基本概念

関連する子テーブル(Referrer)のデータを取得(Load)します。

例えば、以下のような基点テーブル(会員)と子テーブル(購入)の関連があるとします。

会員(1)
購入(3, 6, 7)
会員(2)
購入(2, 9)
会員(3)
購入(1, 4)
会員(4)
購入(5, 8, 10)

まず ConditionBean を使って会員の一覧、会員(1, 2, 3, 4)を取得します。その後、LoadReferrer を使って会員(一覧)に関連する全ての購入(3, 6, 7, 2, 9, 1, 4, 5, 8, 10)を 一括取得 します。取得後、LoadReferrer の処理の中でそれぞれの会員と購入のマッピングをして、DomainEntity のオブジェクトグラフを構築します。

  1. ConditionBeanで会員の一覧を取得(selectListなど)
  2. LoadReferrerで関連する購入の一覧を取得&マッピング

SQLの合計は、基点テーブルの検索一回 + 子テーブルの数(LoadReferrerの回数) となります。上記の例の場合は、"会員(リスト検索) + 購入(LoadReferrer)" で合計 2 回のSQLとなります。このような方式をDBFluteでは、バッチフェッチ と呼んでいます。

LoadReferrer に絡んで、DBFlute には one-to-many の明確なポリシーがあります。

会話上では、ろーどりふぁらぁ と表現します。

子テーブル対応の役割を明確に

これは、あくまで子テーブルのデータ取得であって、子テーブルを使った絞り込み(ExistsReferrer)ではありません。 DBFluteでは、この違いを混同させずに機能として明確に分けています。

また、LoadReferrer は、子テーブルを many として(many のまま)、取得するための機能であり、 子テーブルの導出カラム(とあるカラムの max や sum など、実質的にone-to-oneとなるデータ)の取得は、別途機能 (Specify)DerivedReferrer として用意されています。

実装方法

実装の流れ ※1.1.x (Java8版)

Behaviorの load[referrer-table]() を呼び出し、第一引数に会員(一覧)のデータ、第二引数で子テーブルの検索条件のコールバックを指定します。

e.g. LoadReferrerの実装手順 (Eclipseでコード補完) {MEMBER, PURCHASE} @Java
// まずは普通に会員一覧
List<Member> memberList = memberBhv.selectList(cb -> {
    cb... // 様々な条件
});

// .lo まで打つと関連テーブル選択、続けて Pu (Purchase)
// オーバーロードのメソッドが二つ:
// 第一引数が "リスト型" か "Entity型" の違いだけなので、合う方を選べばOK
memberBhv.loPu
--

// メソッドが補完されて、引数の "memberList" が選択状態に
memberBhv.loadPurchase(memberList, refCBLambda);
--

// tab を一回押して、第二引数の方を "refCBLambda" を選択状態に
// そして、_ll で補完 (DBFlute補完テンプレートが有効なら)
memberBhv.loadPurchase(memberList, _ll);
--

// Lambda引数名は purchaseCB にして...
// 最後にセミコロン ";" を忘れずに
memberBhv.loadPurchase(memberList, purchaseCB -> {
   purchaseCB.query().setPurchasePrice_GreaterEqual(2000);
   purchaseCB.query().addOrderBy_PurchaseDatetime_Desc();
});
for (Member member : memberList) {
    // loadした子テーブルの一覧を取得
    // (LoadReferrerしていない場合はこれが空リスト)
    List<Purchase> purchaseList = member.getPurchaseList();
    ...
}

Lambda引数名では、subCBは使わず "テーブルを識別できるCB名" が推奨されます。

実装の流れ ※1.0.x (Java6版)

e.g. LoadReferrerを使って関連する購入を一括取得 {PURCHASE} @DisplaySql
...
  from PURCHASE dfloc 
 where dfloc.MEMBER_ID in (1, 2, 3, 4)
   and dfloc.PURCHASE_COUNT >= 2000 
 order by dfloc.MEMBER_ID asc, dfloc.PURCHASE_DATETIME desc
e.g. LoadReferrerの実装手順 (Eclipseでコード補完) {MEMBER, PURCHASE} @Java
MemberCB cb = new MemberCB();
cb... // 様々な条件
List<Member> memberList = memberBhv.selectList(cb); // まずは普通に会員一覧

// 1. .lo まで打つと関連テーブル選択
// 2. PL (PurchaseList) で関連テーブル確定
// 3. オーバーロードのメソッドが四つ:
//   A. リスト型引数、ConditionBeanSetupper
//   B. リスト型引数、LoadReferrerOption
//   C. Entity型引数、ConditionBeanSetupper
//   D. Entity型引数、LoadReferrerOption
//  ※ここでは、"A" を選択して enter ("A" の JavaDoc に Example あり)
memberBhv.loPL
--
// メソッドが補完されて、引数の "memberList" が選択状態に
memberBhv.loadPurchaseList(memberList, conditionBeanSetupper);
--
// 1. tab を一回押して、第二引数の方を選択状態に
// 2. "new " (new + 空白一つ) と打って ctrl + space そして enter
memberBhv.loadPurchaseList(memberList, new );
--
memberBhv.loadPurchaseList(memberList, new ConditionBeanSetupper<PurchaseCB>() {

    public void setup(PurchaseCB cb) {
        // TODO Auto-generated method stub
        
    }
})
--
// ctrl (or command) + D で不要な空行やTODOコメントを消して
// 子テーブルを取得するための検索条件を指定
memberBhv.loadPurchaseList(memberList, new ConditionBeanSetupper<PurchaseCB>() {
    public void setup(PurchaseCB cb) {
        cb.query().setPurchasePrice_GreaterEqual(2000);
        cb.query().addOrderBy_PurchaseDatetime_Desc();
    }
}); // セミコロンを忘れずに
--
fore // fore とまで打って ctrl + space そして enter
--
for (Member member : memberList) {
    // 関連する子テーブルの一覧を取得
    // (LoadReferrerしていない場合はこれが空リスト)
    List<Purchase> purchaseList = member.getPurchaseList();
    ...
}

子テーブルのソート条件

ReferrerConditionSetupper で、子テーブルのソート条件を指定できます。このコールバックはサブクエリではありませんので、絞り込み条件に限らず OrderBy が利用できます。業務的にほとんどの場合、ソート条件が必要になると想定されています。

e.g. 会員に関連する購入を購入日時の降順で取得 @Java
...
memberBhv.loadPurchase(memberList, purchaseCB -> {
    purchaseCB.query().addOrderBy_PurchaseDatetime_Desc(); // 購入日時の降順で
});
e.g. 会員に関連する購入を購入日時の降順で取得 @DisplaySql
...
  from PURCHASE dfloc 
 where dfloc.MEMBER_ID in (1, 2, 3, 4)
 order by dfloc.MEMBER_ID asc, dfloc.PURCHASE_DATETIME desc

一つ目のソートキーは、関連のためのカラムが自動的に付与されます。 明示的に指定したソート条件は、二つ目以降のソートキーとして展開されます。 (但し、上記の例で MEMBER_ID 絞り込みが一つだけの場合は、一つ目のソートキー MEMBER_ID のソートは省略される)

子テーブルの親テーブルの取得 (one-to-many-to-one)

ConditionBeanSetupper で、子テーブルのデータ取得条件を指定できます。このコールバックはサブクエリではありませんので、絞り込み条件に限らず SetupSelect も利用でき、子テーブルの親テーブル(one-to-many-to-one)も一緒に取得することができます。

e.g. 会員に関連する購入と共に商品も取得 @Java
...
memberBhv.loadPurchase(memberList, purchaseCB -> {
    purchaseCB.setupSelect_Product(); // 商品も同時に取得
    purchaseCB.query().addOrderBy_PurchaseDatetime_Desc();
});

for (Member member : memberList) {
    List<Purchase> purchaseList = member.getPurchaseList();
    for (Purchase purchase : purchaseList) {
        Product product = purchase.getProduct(); // 商品も取得できる
        ...
    }
}

子テーブルの子テーブルの取得 (one-to-many-to-many)

withNestedReferrer() @since 1.0.5G

1.0.5G より、実装しやすい新たな方法が提供されています。

e.g. 会員と購入とさらに購入支払を取得 @Java
List<Member> memberList = memberBhv.selectList(cb -> { // 会員
    cb... // 様々な条件
});

memberBhv.loadPurchase(memberList, purchaseCB -> { // 購入
    purchaseCB.query().addOrderBy_PurchaseDatetime_Desc();
}).withNestedReferrer(purchaseList > {
    purchaseBhv.loadPurchasePayment(purchaseList, paymentCB -> { // 購入支払
        paymentCB.query().addOrderBy_PaymentDatetime_Desc();
    });
});

従来のやり方 @before 1.1

ここで、LoadReferrerOption の出番です。ConditionBeanSetupper は、シンプルに一階層までの子テーブルを取得するのに向いています。さらに二階層以上先の子テーブルを取得する場合は、LoadReferrerOption を利用します。

LoadReferrerOption に EntityListSetupper のコールバックを設定し、そのコールバックの中でさらに深い階層の LoadReferrer を実装します。

e.g. 会員ステータスに関連する会員とさらに購入を取得 @Java
// 会員ステータス (基点テーブル)
MemberStatusCB cb = new MemberStatusCB();
cb... // 様々な条件
List<MemberStatus> memberStatusList = memberStatusBhv.selectList(cb);

LoadReferrerOption<MemberCB, Member> loadReferrerOption = new LoadReferrerOption<MemberCB, Member>();

// 会員 (一階層目の子テーブル)
loadReferrerOption.setConditionBeanSetupper(new ConditionBeanSetupper<MemberCB>() {
    public void setup(MemberCB cb) {
        cb.query().addOrderBy_FormalizedDatetime_Desc();
    }
});

// 購入 (二階層目の子テーブル)
loadReferrerOption.setEntityListSetupper(new EntityListSetupper<MemberCB>() {
    // ここでさらにネストさせれば、どこまでも階層を辿って子テーブルを取得できる
    public void setup(List<Member> entityList) {
        // 一階層目の子テーブルの Behavior も用意しておく必要あり
        memberBhv.loadPurchaseList(entityList, new ConditionBeanSetupper<PurchaseCB>() {
            public void setup(PurchaseCB cb) {
                cb.query().addOrderBy_PurchaseCount_Desc();
                cb.query().addOrderBy_ProductId_Desc();
            }
        });
    }
});

memberStatusBhv.loadMemberList(memberStatusList, loadReferrerOption);

for (MemberStatus memberStatus : memberStatusList) {
    List<Member> memberList = memberStatus.getMemberList();
    for (Member member : memberList) {
        List<Purchase> purchaseList = member.getPurchaseList();
        ...
    }
}

親テーブルの子テーブルの取得 (many-to-one-to-many)

親テーブルの子テーブルを取得する場合は、基点テーブルに紐付いている親テーブルのリストを取得して、 その親テーブルの Behavior で子テーブルを LoadReferrer する必要があります。

基点テーブルに紐付いている親テーブルのリストは、for文で回せば取得できますが、Behaviorで専用のメソッドも用意されています。 pullout[親テーブル名]()というメソッドを使って取得し、そのリストで LoadReferrer を利用できます。

Behavior - PulloutRelation
e.g. 会員一覧に対して、会員ステータスに紐付くログイン情報を取得 @Java
List<Member> memberList = memberBhv.selectList(cb -> { // 会員ステータス
    cb.setupSelect_MemberStatus();
});
List<MemberStatus> statusList = memberBhv.pulloutMemberStatus(memberList);
memberStatusBhv.loadMemberLogin(statusList, loginCB -> {
    loginCB...
});

枝分かれの子テーブルの取得

それぞれの関連に対して LoadReferrer を呼び出せば、枝分かれの子テーブルが設定されたオブジェクトグラフを構築することができます。

e.g. 会員に関連する購入と会員ログイン情報を取得 @Java
...
memberBhv.loadPurchase(memberList, purchaseCB -> {
    purchaseCB.query().addOrderBy_PurchaseDatetime_Desc();
});
memberBhv.loadMemberLogin(memberList, loginCB -> {
    loginCB.query().addOrderBy_LoginDatetime_Desc();
});

子テーブルの取得カラムの指定

LoadReferrer の中でも SpecifyColumn が利用できます。 ただし、基点テーブルに対する子テーブルのFKカラムは暗黙のうちに SpecifyColumn されます。

Loader方式 @since 1.0.5J

複雑なリレーショナルを辿るのにGood

例えば、one-to-many-to-many, many-to-one-to-many など様々なリレーションを同時に辿るようなとき、様々なテーブルの Behavior をDIしたり、リストを抽出したりと色々な手続きが必要です。ReferrerLoaderでは、そういったリレーションをスムーズに辿れるようになっています。

Behavior の load() メソッド

Behavior の load() メソッドを呼び出し、コールバックで loader を操作します。

e.g. 会員に関連する購入と会員ログイン情報を取得 @Java
List<Member> memberList = memberBhv.selectList(cb -> {
    cb.query()...
});

memberBhv.load(memberList, memberLoader -> {
    // [one-to-many-to-many]
    // 会員から購入を取得、さらに、購入支払を取得
    memberLoader.loadPurchase(purchaseCB -> {
        purchaseCB.query()...
    }).withNestedReferrer(purchaseLoader -> {
        purchaseLoader.loadPurchasePayment(paymentCB -> {
            paymentCB.query()...
        });
    });
    // [many-to-one-to-many]
    // 会員から会員ステータスを経由して、会員ログインを取得
    // (会員ステータスが setupSelect されていることが前提)
    memberLoader.pulloutMemberStatus().loadMemberLogin(loginCB -> {
        loginCB.query()...
    });
});

内部的には、通常の LoadReferrer を呼んでいる

内部的には、通常の LoadReferrer を呼び出しています。Behavior の DI や関連テーブルのリストの抽出と引き渡しなどを内部で解決することで、リレーションと検索条件を指定するだけで load できるようにしています。

外だしSQLでもLoadReferrer

外だしSQLは、基本的にフラット構造でデータを取り扱います。親テーブル(many-to-one や one-to-one)や子テーブルの導出カラムに関しては、基点テーブルと同じ粒度で扱えるので特に問題ありません。 ただ、それでは one-to-many の構造は補完できません。フラット構造に対して紐づく子テーブルを many のまま扱うために、外だしSQLで取得するフラットな CustomizeEntity に、LoadReferrer で取得するオブジェクトグラフの DomainEntity を連携させます。@since 0.9.7.5

基点テーブルの主キーをPKマークに

まず、CustomizeEntity マークにおいて、必ず基点テーブルの主キーを PKマーク に設定します。

e.g. LoadReferrer を利用するためにPKマークを設定 @SQL-File
-- #df:entity#
-- *MEMBER_ID*
select MEMBER_ID, MAX(...), ...
  from PURCHASE
    left outer join ...
 group by MEMBER_ID

もし、メタデータから該当カラムの対応するテーブルの情報が取得できない DBMS (例えば、PostgreSQL, Oracle, SQLServerなど)の場合は、必ずPKマーク上で対応テーブルの情報を指定します。 さらにそのとき、複合PKの場合は、PKマーク上の指定の順序、および、select句でのカラム定義の順序をDB定義上のPKの順序に合わせる必要があります。

e.g. メタデータが取得できないDBMSの場合は関連テーブルを @SQL-File
-- #df:entity#
-- *MEMBER.MEMBER_ID*
select MEMBER_ID, MAX(...), ...
  from PURCHASE
    left outer join ...
 group by MEMBER_ID

CustomizeEntity を利用して LoadReferrer

この設定で Sql2Entity を実行すると、CustomizeEntity に prepareDomain() というメソッドが生成されます。このメソッドを呼ぶと、関連付いている基点テーブルの DomainEntity のインスタンスが、LoadReferrer できる状態で CustomizeEntity 内で保持されます。戻された DomainEntity をリストに詰め込み、そのリストを LoadReferrer で実行します。すると、CustomizeEntity から子テーブルを取得することができます。

e.g. CustomizeEntity を利用して購入を LoadReferrer @Java
List<UnpaidSummaryMember> memberList = memberBhv.outsideSql()...

List<Member> domainList = new ArrayList<Member>(); 
for (UnpaidSummaryMember member : memberList) {
    domainList.add(member.prepareDomain());
}

memberBhv.loadPurchase(domainList, purchaseCB -> {
    ...
});

for (UnpaidSummaryMember member : memberList) {
    List<Purchase> = member.getPurchaseList();
    ...
}

CustomizeEntity のリストをループさせて、DomainEntity のリストを構築するところがちょっとベタですが、こうすることで外だしSQLでも LoadReferrer が利用できます。サポートされる前のバージョンでも、prepareDomain() のソースを参考に ExEntity で同じような実装をすれば実現できます。

prepareDomain() が生成されないときは、PKマークの設定に失敗している可能性があります。 例えば、PKでないカラムを指定してしまっている、メタデータを取得できない DBMS なのに関連テーブルの指定がないなど。また、メタデータを取得できる DBMS であってもSQLの複雑度によっては取得できない可能性もあるかもしれませんので、もし CustomizeEntity のJavaDocコメントに関連テーブルの表示が無い場合は、関連テーブル明示的な指定も試してみると良いでしょう。

メソッド仕様

一つの関連に付き、オーバーロードで四つのメソッドがあります。

  • リスト型引数、ConditionBeanSetupper
  • リスト型引数、LoadReferrerOption
  • Entity型引数、ConditionBeanSetupper
  • Entity型引数、LoadReferrerOption

引数

第一引数は、該当のBehaviorに対応するテーブルのEntity型(のリスト)となります。 第二引数は、該当の子テーブルのEntityやConditionBeanに型付いたものとなります。

第一引数、第二引数共に null を指定した場合は例外です。但し、第一引数で空リストは許容されます(何も処理が実行されないだけ)。 リストの中のEntityのキー値(PK or UQ)は必須です。

戻り値

void型となります。

オーバーライド

オーバーライドして拡張することもできますが、難しいです。

同じリストに対して二度呼び出し

子テーブルの情報は上書きされます。

PKの値の大文字小文字

マッピング処理にてPKの値を利用しますが、大文字小文字は区別せずに処理します。 つまり、大文字小文字区別なしで等しいPK値、例えば "FOO" と "foo" という別々のPK値がある場合はサポートされません。 ただし、ExBehavior でオーバーライド拡張することで、大文字小文字を区別するように変えることはできます。

one-to-manyアプローチのバランス

DBFluteでは、one-to-many のアプローチで明確なポリシーがあります。

一つ目は、データ取得と絞り込みを明確に分けること。これは、子テーブルに対してに限らず、ConditionBean 全体のポリシーに通じる話です。これを混同したまま one-to-many のアプローチの議論をよく聞きますが、DBFluteでは機能自体が明確に分かれています。 (LoadReferrer と ExistsReferrer)

二つ目は、データ取得において、安全であることと安定したパフォーマンスの両方を考慮すること。 DBFluteでは以下のやり方は採用しません。

  • A. Getter が呼ばれたときにSQLを発行して取得 (LazyLoad)
  • B. 基点テーブルのレコードごとにSQLを発行して取得 ("A" の実現方法の一つでもある)
  • C. 一発のSQLで子テーブルも結合して、フラット構造をマッピング

DBFluteとしては、"A" は安全でないと考えています。"何のデータを取得したのか?" をプログラム上で明確にすることが大事であるというポリシーを持っているからです。 取得するのを忘れても動作するので気付かずそのまま、にするケースを懸念しています。完全に任せてもパフォーマンス上の問題が発生しない LazyLoad の機構が作れれば良いですが、それはなかなか難しいことです。現実的なところ、少なくとも DBFlute では、取得するのを忘れたらデータが無くて気付いて、(効率の良い方法で)明確にデータを取得するように実装し直す方が良いと考えています。

また、"B" はパフォーマンス上の問題があります("A" の話とも関連があります)。例えば、50 件の会員一覧があるとして購入を取得する場合、合計で 51 回ものSQLが発行されることになります。

そして、最速と思われる "C" は、実は速度的に安定しません。 単純な会員と購入だけの関係ならば速いですが、会員に対して購入と会員ログインなどと枝分かれの子テーブルを取得しようとした場合、 SQLが返す結果セットは膨大なものとなり、逆に遅くなってしまう可能性があります。実は、昔のDBFluteにはこの機能がありました。HierarchyArranger というクラスがその名残です。実際に業務に照らし合わせると、速い時はものすごい速く処理されますが、ちょっと複雑になったり、扱うデータ量が増えたりすると、 途端に遅くなる現象がありました。その原因はまさしく結果セットの膨張です。

DBFluteは、これらの問題を回避した折衷案の機能 LoadReferrer を提供し、"C" のように最速ではないにしても、やってることが単純でわかりやすく、安全な実装、安定したパフォーマンスを出せるようにしています。