30.7.3. Read-ahead

JBoss での最適化ローディングは read-ahead と呼ばれます。これは、ロードするエンティティの行と、その次の数行を読み込む技術のことで、こういった背景から read-ahead (先読み) という表現となっています。JBoss は主に 2 つのストラテジー (on-find および on-load) を実装して、前項で挙げたローディングの問題を最適化します。read-ahead 中にロードされた余分なデータは、すぐにメモリ内のエンティティオブジェクトと関連付けられません。なぜなら、JBoss においてエンティティは実際にアクセスされるまで実体化されないからです。その代わり、これはプレロードキャッシュに格納され、エンティティにロードされるまで、あるいはトランザクションの終わりが現れるまで、そこにとどまります。以降のセクションでは、read-ahead ストラテジーについて説明します。

30.7.3.1. on-find

on-find ストラテジーは、クエリが呼び出されると、追加のカラムを読み込みます。クエリが on-find 最適化されている場合、クエリが実行されると JBoss は次のクエリを実行します。
SELECT t0_g.id, t0_g.name, t0_g.nick_name, t0_g.badness 
    FROM gangster t0_g
    ORDER BY t0_g.id ASC
必要なデータはすべてプレロードキャッシュ内に入るので、クエリ結果を反復している間、追加のクエリを実行する必要はありません。このストラテジーは、少量のデータを返すクエリに対しては有効ですが、メモリに大量の結果セットをロードしようとしている場合には非常に効率的ではありません。次のテーブルは、このクエリーの実行を示したものです。

表30.2 on-find 最適化クエリ実行

id name nick_name badness hangout organization
0 Yojimbo Bodyguard 7 0 Yakuza
1 Takeshi Master 10 1 Yakuza
2 Yuriko Four finger 4 2 Yakuza
3 Chow Killer 9 3 Triads
4 Shogi Lightning 8 4 Triads
5 Valentino Pizza-Face 4 5 Mafia
6 Toni Toothless 2 6 Mafia
7 Corleone Godfather 6 7 Mafia
クエリーの read-ahead ストラテジーと load-group は、query 要素に定義されます。read-ahead ストラテジーが query 要素に宣言されていない場合は、entity 要素または defaults 要素に宣言されたストラテジーが使用されます。on-find 設定は次のとおりです。
<jbosscmp-jdbc>
    <enterprise-beans>
        <entity>
            <ejb-name>GangsterEJB</ejb-name>
            <!--...-->
            <query>
                <query-method>
                    <method-name>findAll_onfind</method-name>
                    <method-params/>
                </query-method>
                <jboss-ql><![CDATA[
                 SELECT OBJECT(g)
                 FROM gangster g
                 ORDER BY g.gangsterId
                 ]]></jboss-ql>
                <read-ahead>
                    <strategy>on-find</strategy>
                    <page-size>4</page-size>
                    <eager-load-group>basic</eager-load-group>
                </read-ahead>
            </query>
        </entity>
    </enterprise-beans>
</jbosscmp-jdbc>
on-find ストラテジーには、選択されたすべてのエンティティについて追加データをロードしなければならないという問題があります。Web アプリケーションでは通常、ページ上で固定された数の結果だけがレンダリングされます。プレロードされたデータはトランザクションの長さだけ有効であり、1 トランザクションは Web HTTP の 1 ヒットに制限されるので、プレロードされたデータの大半は使用されません。次項で説明する on-load ストラテジーでは、この問題の影響を受けません。
30.7.3.1.1. Left join read ahead
Left join read ahead は、on-findread-ahead ストラテジーが強化されたものです。1 つの SQL クエリで、ベースインスタンスからのフィールドだけでなく、CMR ナビゲーションによりベースインスタンスから到達できる関連インスタンスもプレロードすることができます。CMR ナビゲーションの深さに対する制限はありません。また、ナビゲーションおよびリレーションシップタイプのマッピングで使用される CMR フィールドのカーディナリティに対する制限もありません。すなわち、外部キーと関連テーブルマッピングスタイルの両方がサポートされます。それでは、例をいくつか見ていきましょう。エンティティとリレーションシップの宣言を以下に示しています。
30.7.3.1.2. D#findByPrimaryKey
エンティティ Dがあるとします。findByPrimaryKey に対して生成される一般的な SQL は、次のようになります。
SELECT t0_D.id, t0_D.name FROM D t0_D WHERE t0_D.id=?
findByPrimaryKey の実行中に、2 つのコレクション値 CMR フィールド bs および csもプレロードすることにします。
<query>
    <query-method>
        <method-name>findByPrimaryKey</method-name>
        <method-params>
            <method-param>java.lang.Long</method-param>
        </method-params>
    </query-method>
    <jboss-ql><![CDATA[SELECT OBJECT(o) FROM D AS o WHERE o.id = ?1]]></jboss-ql>
    <read-ahead>
        <strategy>on-find</strategy>
        <page-size>4</page-size>
        <eager-load-group>basic</eager-load-group>
        <left-join cmr-field="bs" eager-load-group="basic"/>
        <left-join cmr-field="cs" eager-load-group="basic"/>
    </read-ahead>
</query>
left-join では、一括読み込みを行いたいリレーションを宣言します。生成される SQL は次のようになります。
SELECT t0_D.id, t0_D.name,
       t1_D_bs.id, t1_D_bs.name,
       t2_D_cs.id, t2_D_cs.name
  FROM D t0_D
       LEFT OUTER JOIN B t1_D_bs ON t0_D.id=t1_D_bs.D_FK
       LEFT OUTER JOIN C t2_D_cs ON t0_D.id=t2_D_cs.D_FK
 WHERE t0_D.id=?
固有の id を持つ D では、それに関連するすべての BCをプリロードし、それをデータベースからではなく先読みキャッシュからロードするインスタンスにアクセスすることができます。
30.7.3.1.3. D#findAll
同様に、D への findAll メソッドはD 関連をすべて選択しますが、この findAll メソッドを最適化することができます。通常の findAll クエリは次のようになります。
SELECT DISTINCT t0_o.id, t0_o.name FROM D t0_o ORDER BY t0_o.id DESC
リレーションをプレロードするには、left-join 要素をクエリーに追加するだけで結構です。
<query>
    <query-method>
        <method-name>findAll</method-name>
    </query-method>
    <jboss-ql><![CDATA[SELECT DISTINCT OBJECT(o) FROM D AS o ORDER BY o.id DESC]]></jboss-ql>
    <read-ahead>
        <strategy>on-find</strategy>
        <page-size>4</page-size>
        <eager-load-group>basic</eager-load-group>
        <left-join cmr-field="bs" eager-load-group="basic"/>
        <left-join cmr-field="cs" eager-load-group="basic"/>
    </read-ahead>
</query>
生成される SQL は次のとおりです。
SELECT DISTINCT t0_o.id, t0_o.name,
                t1_o_bs.id, t1_o_bs.name,
                t2_o_cs.id, t2_o_cs.name
  FROM D t0_o
       LEFT OUTER JOIN B t1_o_bs ON t0_o.id=t1_o_bs.D_FK
       LEFT OUTER JOIN C t2_o_cs ON t0_o.id=t2_o_cs.D_FK
 ORDER BY t0_o.id DESC
これで、このシンプルな findAll クエリは、各 D オブジェクトに対して、関連する B および C オブジェクトをプレロードするようになりました。
30.7.3.1.4. A#findAll
それでは、より複雑な設定を見てみましょう。ここでは、いくつかのリレーションと共にインスタンス A をプレロードしようと思います。
  • CMR フィールド parentA から到達するその親 (セルフリレーション)
  • CMR フィールド bA から到達する B、および CMR フィールド cB から到達する関連の C
  • 今回は CMR フィールド b2 で A から到達する B、およびそれに関連付けられた、CMR フィールド c で B から到達する C
参考までに、標準のクエリーは次のとおりです。
SELECT t0_o.id, t0_o.name FROM A t0_o ORDER BY t0_o.id DESC FOR UPDATE
次のメタデータは、プレロードプランを記述したものです。
<query>
    <query-method>
        <method-name>findAll</method-name>
    </query-method>
    <jboss-ql><![CDATA[SELECT OBJECT(o) FROM A AS o ORDER BY o.id DESC]]></jboss-ql>
    <read-ahead>
        <strategy>on-find</strategy>
        <page-size>4</page-size>
        <eager-load-group>basic</eager-load-group>
        <left-join cmr-field="parent" eager-load-group="basic"/>
        <left-join cmr-field="b" eager-load-group="basic">
            <left-join cmr-field="c" eager-load-group="basic"/>
        </left-join>
        <left-join cmr-field="b2" eager-load-group="basic">
            <left-join cmr-field="c" eager-load-group="basic"/>
        </left-join>
    </read-ahead>
</query>
生成 SQL は次のようになります。
SELECT t0_o.id, t0_o.name,
       t1_o_parent.id, t1_o_parent.name,
       t2_o_b.id, t2_o_b.name,
       t3_o_b_c.id, t3_o_b_c.name,
       t4_o_b2.id, t4_o_b2.name,
       t5_o_b2_c.id, t5_o_b2_c.name
  FROM A t0_o
       LEFT OUTER JOIN A t1_o_parent ON t0_o.PARENT=t1_o_parent.id
       LEFT OUTER JOIN B t2_o_b ON t0_o.B_FK=t2_o_b.id
       LEFT OUTER JOIN C t3_o_b_c ON t2_o_b.C_FK=t3_o_b_c.id
       LEFT OUTER JOIN B t4_o_b2 ON t0_o.B2_FK=t4_o_b2.id
       LEFT OUTER JOIN C t5_o_b2_c ON t4_o_b2.C_FK=t5_o_b2_c.id
 ORDER BY t0_o.id DESC FOR UPDATE
この設定では、別のデータベースをロードすることなく、見つかった A のインスタンスから CMR をナビゲートすることができます。
30.7.3.1.5. A#findMeParentGrandParent
次に別のセルフリレーションの例を示します。あるインスタンス、その親、祖父母および曽祖父母を 1 つのクエリでプレロードするメソッドを書くことにします。これを行うのに、ネストされた left-join 宣言を使用します。
<query>
    <query-method>
        <method-name>findMeParentGrandParent</method-name>
        <method-params>
            <method-param>java.lang.Long</method-param>
        </method-params>
    </query-method>
    <jboss-ql><![CDATA[SELECT OBJECT(o) FROM A AS o WHERE o.id = ?1]]></jboss-ql>
    <read-ahead>
        <strategy>on-find</strategy>
        <page-size>4</page-size>
        <eager-load-group>*</eager-load-group>
        <left-join cmr-field="parent" eager-load-group="basic">
            <left-join cmr-field="parent" eager-load-group="basic">
                <left-join cmr-field="parent" eager-load-group="basic"/>
            </left-join>
        </left-join>
    </read-ahead>
</query>
生成される SQL は次のようになります。
SELECT t0_o.id, t0_o.name, t0_o.secondName, t0_o.B_FK, t0_o.B2_FK, t0_o.PARENT,
       t1_o_parent.id, t1_o_parent.name,
       t2_o_parent_parent.id, t2_o_parent_parent.name,
       t3_o_parent_parent_parent.id, t3_o_parent_parent_parent.name
  FROM A t0_o
       LEFT OUTER JOIN A t1_o_parent ON t0_o.PARENT=t1_o_parent.id
       LEFT OUTER JOIN A t2_o_parent_parent ON t1_o_parent.PARENT=t2_o_parent_parent.id
       LEFT OUTER JOIN A t3_o_parent_parent_parent 
            ON t2_o_parent_parent.PARENT=t3_o_parent_parent_parent.id
 WHERE (t0_o.id = ?) FOR UPDATE
left-join メタデータを取り除けば、次のようになります。
SELECT t0_o.id, t0_o.name, t0_o.secondName, t0_o.B2_FK, t0_o.PARENT FOR UPDATE