S2JDBC / クエリを投げる(タイプセーフ) / OneToMany 結合の注意

S2JDBC / クエリを投げる(タイプセーフ) / OneToMany 結合の注意

パフォーマンス

基本的なことだが OneToMeny 結合を leftOuterJoin でもってきたとすると、左側テーブル(select() の Entity)をベースに外部キーで結合していくことになる。 このような場合、その外部キー的なフィールドにインデックスが無いと、状況によりパフォーマンスが劣化する。

普通の SQL とは違う結合の挙動

ここの感覚で少し通常の SQL と違うのは主体はあくまで from で指定したエンティティということだ。

普通ならば結合先の粒度が細かい方が主体になる。 なのでレコード数は結合の前後関係なく粒度が細かいテーブルがメインになるはず。

しかし S2JDBC ではリターンされるのは from で指定しているエンティティであり1レコードになる。 これは S2JDBC が内部でゴニョゴニョしてくれていて値を詰め替えてくれているからだ。

詰め替えは巧妙に行われていて1レコードから発生した情報が完全に1インスタンスに対応している。 Hoge が piyoList に Piyo の関連を、Piyo が hoge に Hoge への関連を持っているがこれを合わせて

import static com.aaa.bbb.entity.Names.*;
import static org.seasar.extension.jdbc.operation.Operations.*;
//----- cutdown --------
 
Hoge hoge = jdbcManager
    .from(Hoge.class)
    .leftOuterJoin(hoge().piyoList())
    .leftOuterJoin(hoge().piyoList().hoge())
    .where(
        eq(hoge().piyoId(), 10),
        eq(hoge().name(), "たなか")
    )
    .getSingleResult();

と書くこともできるのだ。

となると

hoge1
  +--piyoList
     +--piyo1
        +--hoge
           +--hoge1
     +--piyo2
        +--hoge
           +--hoge1
     +--piyo3
        +--hoge
           +--hoge1

ここで hoge1 というインスタンスが4回登場してきている。これが全部同値ではなく同一ということになる。

同一なので JSONIC などでJSON化しようとすると循環参照で無限ループに入る可能性がある。

OneToMany を持つ Entity において where に ManyToOne の関連側のフィールドを条件を使うと、その条件に合致する以外のレコードは OneToMany の プロパティに格納されない。

例えば「山田」という姓の社員がいる会社エンティティを所属社員ごと OneToMany で引っ張ってくるとして、条件に「山田」を入れると、確かに「山田」さんが所属する会社エンティティのリストは取れるかもしれないが、OneToMany のプロパティには山田さんしか入っていないことになる。他の社員は居ない。

このような場合はこの結果によって再び取り直すという動作が必要になる。

count, offset, limit の挙動の違い

↑のように主体が from メソッドが取るクラスになり、そのクラスが OneToMany の関連を持っていて、さらにそこで結合した場合、 getResultList で得る List の size とは from で指示した Entity の数になる。(なっていて欲しい)

しかし面倒なのが、getCount でレコード数を取ったり、 offset や limit メソッドで扱うのは S2JDBC で詰替えを行う前の SQL で取得したレコードの考え方なのである。

ここで OneToMany 結合があると、from 指定の Entity のインスタンス量よりも、SQL で出てくる結果レコード数が増えてしまう場合がある。 こうなると、 offset や limit の指定と、期待する結果が全然合わなくなってしまう。

S2JDBC の結合機能の使って、値を芋づるで取りたいという要件と、一覧をページングしたいみたいな要件が両立しなくなる。

これは OneToMany の関連の値を使いたい、検索条件に入れたい要望があると一覧には現実問題使えないという困ったことになる。 S2JDBCは素晴らしいORマッパーだがこの一点のために非常に使いにくくなってしまう。

一応回避する手段どしてはまず普通にタイプセーフでないSQLで取得しその Entity の PK だけを distinct で押しつぶして取得し、とれた結果の PK のリストを今度はタイプセーフでin指定で取得するというやり方。そして OrderBy には MySQL ならばこのように任意のID順に並ぶようにする。これで元のクエリの結果の Order も引き継げる。

.orderBy("FIELD(" + hoge().id() + "," + StringUtils.join(idList, ",") + ")")

これがいい手段かどうかわからんが、一応これでできるといえばできる。IDを直指定している、っでページングするぐらいなのでそんなにレコード数が多くないのでこれでOKという妥協。

別のやり方として、OneToMany 結合が必要な検索条件だけ先にタイプセーフで取得してしまって、その結果から取り出した PK の List を本編の in 条件として使うと、一応全部の過程でタイプセーフで作ることができるようになる。これの場合は、条件によっては毎回全IDの in 検索が走るようなパターンになり性能劣化がどの程度おきるのかが気になるところである。

leftOuterJoin だからといって左側の存在が保証されるわけではない

SQL の発生を考えると当たり前のことなのだが、OneToMany の概念が通常の SQL に無いので勘違いしてしまう。

from に指定した Entity に対して leftOuterJoin で結合指定して OneToMany の関連に値をマッピングするわけだが、ここで from で指定した Entity を制限するために Where 句に Many 側の値を条件に指定したとする。

ここで問題になるのが、Many 側で条件に合致しないレコードが落ちてしまって OneToMany 関連の中身も完品で揃わないということがある。 これは、from の Entity の PK を in で取り直すということで一応対処可能。

さらに考えないといけないのが、Many 側にまったく条件に合致するものが無かった場合に One 側の結果が発生せず、One 側そのものが結果から無くなってしまうということになる。 これは条件によっては回避不能なわけだが、実用上は leftOuterJoin 自体に結合条件を記述することで SQL 側の ON を活かすことができて左側を維持できる。

s2/s2jdbc/throw_query_typesafe/join_onetomeny.txt · 最終更新: 2021-03-24 10:34 by ore