Hibernateを使用した ― O/Rマッピングは漢(オトコ)の浪漫
サブタイトルには今のところ意味は無いです。
Hibbernate イン アクション(及びリバイズドエディションの Java Persistence with Hibernate)は、私が人からお勧めの本を聞かれた際の回答としてよく登場するもののうちの1冊です(ちなみに~インアクションの方はすでに廃版らしく、また、Java Persistence~の方は訳書が出ていません…)。今回のQconでGavin Kingさんが来日されると聞いて、これはサインを貰わねば、と2冊スタンバイしていたのですが取りやめになってしまって非常に残念でした。
そんな行動とは裏腹に、実のところ私はHibernateをガッツリ使ったことがなく、それどころかSQL文も SELECT * FROM ○ WHERE △ = ×; 以外の書式は入門書を見ながらでないと入力できないくらいDBに縁がありませんでした。
しかし今回、遂に、Hibernateに触れる機会がありましたので、実際に使用してみて詰まったところを記載しておこうと思います。
ちゃんと等価性を考慮したのにうまく動かない
先日のエントリでも触れた等価性の話です。Hibernateでは(その他JPA実装プロダクトでも)、
- @Idアノテーションをつけたフィールドは人工キーにし、RDB上でprimary key制約をつける
- 人工キーとは別に自然キーを持たせ、RDB上ではunique制約をつける
- 等価性、つまりequalsメソッドは自然キーを用いて実装する
というのが典型的なエンティティの実装かと思います。しかしこのように適切にequalsを実装し、これに沿う形でhashCodeを実装したとしても、@OneToManyでEager Fetchを行うと期待通り動作してくれません。これが以下のバグです。
実際の動作はコードが添付されているのでそちらを見てもらうことにして、ポイントは以下になります。
1: @Entity(name = "Container")
2: private static class Container
3: {
4: // 中略
5: @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST, mappedBy = "container")
6: Set<Item> items = new HashSet<Item>();
7: }
ここで”fetch = FetchType.EAGER”を取り除いてやれば(@OneToManyのデフォルトはLazyなので)期待通りの動作になります。
プライオリティはMajarとして登録されていますが、Javaの常識/ORMの常識が通用しないという意味で精神的にはクリティカルなバグでした…(わざわざWikipediaの短い説明文の中にも注釈うってるくらい重要なのに!)
「まあバグだったら仕方が無い、Lazy Fetchにするか」と思っても、次のバグが追い打ちをかけます。
これは、あるエンティティのプロパティ(フィールド)をクエリ条件の対象にしようとしてエイリアスを設定すると、エイリアスが設定されたエンティティは強制的にJOINされてくっついて取れてきちゃう、つまり強制的にEarger Fetchされてしまうので、合わせ技で回避方法が無くなります…
ちなみにHHH-3538(の重複元として登録されているHHH-2049)は、このエントリを書いている当日リリースされたバージョン3.6.4で修正されたようです。このバージョンを使えるわけではないので、これはこれで悔しいです…
[追記]
…と書いた矢先、HHH-3538とHHH-2049のDuplicateが取り消されて、このバグはReopenedになりました。実際は重複ではなくこちらはまだ未解消だったということのようですね。
DELETE –> INSERT したいのに INSERT –> DELETE になってしまう
理由は様々のようですが、UPDATEではなくDELETEしてからINSERTしたい、というような要望を時々耳にします(心の中ではDELETE-INSERTアンチパターンと呼んでいます。前述の通りRDBの実装にあまり関わったことが無いので本当にアンチパターンなのかどうかは分からないのですが)。で、そのDELETE-INSERTアンチパターンをHibernateで実現しようとしても、HibernateはINSERT優先なのでDELETEする前にINSERTが実行されprimary key制約違反になります。
こういう事象が、DELETE-INSERTアンチパターンを明示的に適用しなくても、通常の操作で発生してしまう、というバグです。
親子関係にあるようなエンティティが@OneToManyであり、それを@JoinTable(デフォルト戦略です)で実現しているときに子の挿げ替えを行うとunique制約違反になる、というものです。join tableはparent_idとchild_idというようなカラムを持っており、JPAの機能で自動的にテーブルを生成するとchild_idにunique制約が張られます。そしてHibernateは先に述べたようにINSERT優先、つまり新しい関連を先にRDBに反映させようとするのでchild_idが重複します。
unique制約を外したり、本当は@OneToManyなんだけど@ManyToManyに変えてしまうことで回避できると思うのですが、純潔派の方にとっては気持ちが悪いでしょう。
重複元HHH-1268のタイトルを見ると、削除だけでも発生するようですね…
[追記: 書籍Hibernate辞典を読み直していたら、「07-04-02 一対多関連の洗い替えを行いたい」という節で、上で「挿げ替え」と呼んでいる処理を「洗い替え」という名前で紹介していますね。検索してみると、そこそこ通用しそうな呼び名のように思われます。]
Listで@OneToManyを実現するとINSERTが2回走ってしまう
トランザクション内では特定の順番に並んでいてほしいけれど、トランザクションを抜けて再度ロードしたときには順番はどうでもいい、みたいなことを実現したいことがありまして、Listがまさにそうじゃないか、とやってみたところ見事にかかりました。(Hibernateネイティブの機能でListを使う場合には順序を保持して永続化することもできますが、JPA経由の場合はSetと同じく順序は保持されません。)
報告日も比較的最近、プライオリティもCriticalと、結構大物釣り上げたかな?と見つけた当初は思っていたのですが、実はあんまり記憶にありません…まあそんなことやろうと思うのは稀ですしね。
と、HibernateってORMの雄であり、結構枯れてるプロダクトかと思いきや、これが中々血気盛んです、というお話でした。他にも小さなバグには出会ったのですが、さすがに私が少し扱って見つけた範囲ではすべて報告済みのものばかりでした。
最後に。バグではないのですが。
LazyInitializationExceptionの怪
HibernateについてWebで検索していると、結構LazyInitializationException(LIE)で困ったというような報告がなされています。実際に使用してみるまではこれが結構疑問で、「駄目な使い方ってすぐわかりそうなもんなのになあ」なんて考えていました(マルチスレッドが絡む場合は別として)。
しかし他の問題が絡むと問題が厄介になることに気付きました。
トランザクション開始 –> インスタンスAのメソッド実行(処理1) –> インスタンスBのメソッド呼び出し(処理2) –> インスタンスAに戻ってくる(処理3) –> トランザクション終了
というような流れの中で、処理1でEntityManagerからエンティティをもらって、処理2でいくつかの計算を行った後、処理3でエンティティの関連先を参照しようとするとなぜかLIEが。しかも発生したりしなかったり。
Bを調べてみると、Session(JPAを使っていますので正確にはEntityManager)でクエリを実行した際に発生した例外を握りつぶしていることがわかりました。
Hibernateドキュメントの13.2.3.節「例外ハンドリング」にはこのように書かれています。
Session
が例外 (SQLException
を含む) を投げた場合、直ちに、データベーストランザクションをロールバックし、Session.close()
を呼び、Session
インスタンスを破棄すべきです。Session
のいくつかのメソッドは、セッションの状態を 矛盾したまま にします。 Hibernate が投げた例外を、回復できるものとして扱うことはできません。finally
ブロックの中でclose()
を呼んで、Session
を確実に閉じてください。
なので、インスタンスB自身は例外を握りつぶしても問題なかったからそのような実装になっていたのでしょうが、インスタンスAは自分のあずかり知らぬところで関連先が参照できないような状況になっていた…ということが起こりえます。
また、LIEが発生したりしなかったりする原因は、処理2の中でループで関連先を参照し、ある条件にヒットしたらbreakする、というような処理を行っていました。参照する先はHashSetで持っているので、処理2によってfetchされる関連先は実行の都度変わります。このため、たまたま処理3が参照する関連先エンティティを処理2によって事前にfetchしているような場合であればLIEが発生しない、という事象でした。
実行する度にエラーの有無が変わる、というのはC言語時代のバッファオーバランを彷彿とさせますね。しかもこのLIEの問題を調査しようとしてデバッグプリントを追加したりデバッガでステップ実行しながら関連先エンティティを調べたりしているとその処理によってfetchが行われ現象が再現しなくなる、問題の原因となるコード記述箇所と問題が発生する箇所が異なる、といようなことも然りです。
« NetBeans7上でJDK7を用いてEclipseと結果が異なっていたコンパイラの挙動を確認する | トップページ | JOINが遅すぎて泣ける ― MySQLは漢(オトコ)のコンピュータ道? »
この記事へのコメントは終了しました。
« NetBeans7上でJDK7を用いてEclipseと結果が異なっていたコンパイラの挙動を確認する | トップページ | JOINが遅すぎて泣ける ― MySQLは漢(オトコ)のコンピュータ道? »
コメント