例外ハンドリング

業務的に扱うべき例外

業務的に扱うべき例外は、排他制御に関する例外です。

それ以外の例外は、基本的にはデバッグ用の例外(システムエラー扱い)となります。 時に特別な理由で(ピンポイントで)業務的な分岐のために利用する例外を除いては、例外の種類を意識することなく、 例外メッセージとスタックトレースをシステム管理用のログに出力して、いざ問題が発生したときの有効な手がかりとすると良いでしょう。

排他制御の例外

排他制御の業務的なスコープ次第で変わりますが、基本的には以下の二つを想定しています。

  • org.seasar.dbflute.exception.EntityAlreadyUpdatedException
  • org.seasar.dbflute.exception.EntityAlreadyDeletedException

EntityAlreadyUpdatedException

Behaviorの(排他制御ありの) update()、delete() などにおいて、すれ違い(処理直前に別の人が更新した場合など)が起きたときに発生する例外です。 (主に画面アプリからのリクエスト時に)この例外が発生したときは、すれ違いが起きたことを業務的に通知する必要があります。 アプリケーションで利用しているフレームワークの仕組みを利用して、統一的なハンドリングをすることが推奨されます。

逆に、業務的にしっかりとハンドリングされるのであれば、この例外のメッセージは(本番環境の)ログとして残す必要性はあまりないと考えられます。

EntityAlreadyDeletedException

一件検索を行う selectEntity() や、update()、delete() などにおいて、対象レコードが存在しなかったときに発生する例外です。 この例外は、開発環境と本番環境で意味が異なります。

開発環境
検索条件のバグ or データバグ
本番環境
業務的なすれ違い (処理直前に別の人が削除した場合など)

この例外を実際に業務的なすれ違いとして扱うかどうかは、 アプリで考慮する排他制御のスコープ、業務的に(かつ、仕組み的に)あり得るのか否か、などに左右されます。 もし、業務的なすれ違いとして扱うときは、EntityAlreadyUpdatedException と同様の考慮をする必要があります。

例外メッセージのアプリデータ

DBFluteが発生させる例外メッセージ(主にSQLの例外)には、デバッグ作業の効率化のために、アプリデータを含むことがあります。 例えば、登録処理で一意制約違反が発生したときには、EntityAlreadyExistsException の例外メッセージに、insert 文の(カラムデータが埋め込まれた)表示用SQLが含まれます。

これがあることによって、ディベロッパーは例外が発生したときに、より迅速にその原因を探ることができるようになります。 開発時はもちろんのこと、再現環境の構築がしづらく状況が掴みにくい本番環境 における不具合の対応においてこそ重宝します。 もし、(本番環境で)何のデータで発生した登録エラーなのかどうかがわからなかったら、問題に直面している人は途方に暮れるでしょう。

ただし、以下の項目を 両方とも 満たすようなシステム状況においては、ちょっと注意が必要です。

機密性の高い情報が暗号化されていない
機密性の高い情報(例えば、個人情報など)が、DB上に暗号化されずに格納されている。
ログ出力先のセキュリティ管理の信用度が低い (誰でもログを見れてしまうなど)
ログ出力先(例えば、ログファイル)の、インフラ的なセキュリティ管理の信用度が低く、機密性の高い情報を暗号化せずに出力することが認められていない。

この両方を満たすような状況、そもそもこのような状況自体が推奨されません。 機密性が高いのであれば、DB上でも暗号化されていた方が安全でしょう。 また、ログファイルもサーバ上で(閲覧する際の手順も含めて)厳密に権限管理されるべきでしょう。 ただ、どうしても上記のような状況になってしまった場合のために、幾つかの対策が考えられます。

A. 本番環境のログを暗号化して出力

例外メッセージにアプリデータを含めているのでは DBFlute だけとは限りません。アプリ自体が例外メッセージにアプリデータを含めている可能性 は十分にあります。なので、本番環境のログ自体を暗号化して出力するようにすることが一番です。

例えば、Logback や Log4j の独自の Appender を作成して暗号化してからログに出力するような仕組みを本番環境にだけ設定するのも手段の一つですし、インフラの仕組みで自動でログファイルが暗号化されるようにするのも手段の一つでしょう。 もちろん、暗号キーは厳密に管理する必要があります。

B. (せめて)DBFluteの例外メッセージをマスク

"A" もどうしてもできないという状況の場合(基本的に推奨されません)、もしくは、暗号化した情報ですらログに出力するのが認められていない場合、 アプリ実装自体が例外に含めているアプリデータに関してはここで議論しようがないので省略させて頂きますが、せめて、DBFlute の例外メッセージをマスクすると言った場合に、以下の方法で実現できます。

  1. littleAdjustmentMap.dfprop にて InvokeAssistant を独自のものに
  2. 独自の InvokeAssistant で 独自の SQLExceptionHandler を利用するように
  3. 独自の SQLExceptionHandler でオーバーライドを利用して例外メッセージ内容をマスク

DIコンテナによっては、DBFluteで自動生成されているDIコンフィグファイルもしくはクラスを拡張する方が簡単なケースもあります。 例えば、Springであれば DBFluteBeansJavaConfig を継承したり、Lasta Diであれば、dbflute+invokerAssistant.xml で継承したりすると良いでしょう。

当然のことですが、先程述べた例外メッセージの利便性が失われる ことがトレードオフであることを必ず意識してください。 抑制していることをディベロッパーなど運用後に不具合を対応する人にしっかり通知しておくことも大事でしょう。 本番環境だけ抑制するというような分岐も検討すると良いでしょう。

また、幾つかの例外では、ConditionBean の getDisplaySql() と、Entity の toString() を利用しています。基本的には、明らかなアプリの実装バグの時、もしくは、(排他制御における)すれ違いが発生した時の例外のため、 前者であればそもそもアプリが全く動かない(本番に適用できない)、後者であれば例外メッセージはログには出力しない(する必要がない)、 ということで意識する必要性はほとんどないものですが、厳密さを求める場合は、以下のメソッドをExクラスにてオーバーライドして、 出力される項目が(本番環境だけ)絞り込まれるようにすることで抑制します。

ConditionBean
getDisplaySql()
Entity
buildColumnString() ※toString()が内部で呼び出している

機密性の高い情報を保持しているテーブルは多くはないと考えられるため、このやり方でピンポイントに抑制すると良いでしょう。 また、アプリでこれらメソッドを利用して例外メッセージを構築している場合にも同時に有効です。

(再帰可能な)自動の単体テストをしっかり書く

かなりDBFluteの内部に手を加えることになるため、アップグレード時などに期待する動きをチェックできる(再帰可能な)自動の単体テストをしっかり書くことが大前提です。 大抵の変更はコンパイルエラーですぐに検知するができますが、I/Fは変わらずに内部の動きだけが変わる可能性も(ここまで深い領域であれば)なきにしもあらずです。

さらに厳密なことを考えると

以下のことを忘れずに。

  • (アプリが)埋め込み変数コメントなどでSQL自体にアプリデータを埋め込んでる可能性
  • JDBCドライバの例外メッセージにアプリデータが含まれている可能性(JDBCドライバ次第)

Exampleのススメ

dbflute-ymir-example では、Ymir の仕組みに従って、DBFluteの(主に排他制御の)例外がハンドリングされるようになっています。

DBFlute Example - 他のフレームワーク