👻

Hibernateで例外の原因となった処理を見つける方法

2022/04/24に公開

TL;DR

Hibernate は、PoEAA の Unit of work パターンを利用しているため、flush 時に発生する SQLException の原因となったクエリーを特定し難い問題があります。この問題が起きた場合に、処理内容を dao.saveAndFlush()em.flush() への書き換えるアドホックな対応で終わらせたり、ログを大量に仕込んで調べているエンジニアが多いようですが、実は適切かつ簡単に調べられる方法があります。

  • ActionQueue を処理している所にブレークポイントを張る
  • コードを変更すると例外が消えたり変わったりするので避けるべき

Hibernateで原因クエリーの特定が難しい理由

Hibernate は、Unit of work を実現するため、CRUD 処理を ActionQueue に蓄積し、flush 発生時に溜まっている CRUD 実行します。これを実現するため、Hibernate は、dao.save()dao.saveAll() のタイミングでは、対象オブジェクトを ActionQueue に enqueue するだけで、SQL の生成や評価をしません。この結果、エラーの発生は flush が発生するタイミング、例えば『dirty フラグが立っている managed オブジェクトを select する場合』や『トランザクション境界を出る場合』に先送りされ、開発者の直感とは異なるタイミング(例外の原因箇所と発生箇所が遠く離れる)で、エラーを検知することになります。

そして、SQLException の原因を特定するために dao.find*() をログ出力するコードを追加すると、このログ出力により flush が誘発され例外が出なくなる、または例外が変わります。 その結果、エラー原因を特定できず、混乱に拍車が掛かる悪循環となります。

ブレークポイントを張る場所

上記の説明を読めば、コードを書き換えて調査するのではなく、ActionQueue で例外処理をしている箇所にブレークポイントを張れば良いと、理解できたのではないでしょうか。 実際にブレークポイントを張る場所は、ActionQueue ですが、この位置は、Hibernate のバージョンによって若干異なります。

  • Hibernate 4.3 未満
    org.hibernate.ejb.TransactionImpl.setRollbackOnly()
  • Hibernate 4.3 以降
    org.hibernate.jpa.internal.TransactionImpl.setRollbackOnly()
  • Hibernate 5.0 以降
    org.hibernate.engine.transaction.internal.TransactionImpl.setRollbackOnly()

まとめ

Hibernate で、SQLException の原因箇所を簡単に特定する方法について説明しました。コードを書き換えたり、ログを仕込んだりすると、本来の例外を捕捉できなくなる可能性が出るだけでなく、コードの戻し忘れなどのリスクもあります。Hibernate に苦手意識のある人も多いようですが、仕組みや考え方を理解することで、開発生産性は格段に上がりますので、毛嫌いせずに使いこなして行けると良いですね。

Discussion