Effective Javaの独自解釈です。
第3版の項目73について、自分なりにコード書いたりして解釈してみました。
概要
- 下位レイヤーの例外はそのまま出力させるのではなく、上位レイヤーの概念に応じた例外に翻訳することで以下のメリットが得られる。
- ログ調査しやすくなる
- さらに上位のレイヤーでの例外ハンドリングがしやすくなる
- 実装で例外発生を確実に防げるなら、翻訳は考えず例外発生そのものを防ぐ。
説明
以下の環境で実装したコードで説明する。
- Java 11
- Doma 2.19.2
- Spring Boot 2.3.3
- MySQL 5.7
DB挿入時における例外出力の例
DBにUSER
テーブルがあるとし、レコードを新規追加することを考える。
USER
テーブルには主キーとしてid
カラムを持っている。
Java側の関連クラスは以下の通り。(importなど略)
Entity
public class User {
private String id;
private String name;
}
Dto
public class CreateUser {
private String id;
private String name;
}
Daoインタフェース
public interface UserDao {
@Insert
int insert(User user);
}
下位レイヤーの例外をそのまま出力させた場合
既にUSER
テーブルに存在するid
を持つcreateUser
を引数に入れ、以下のサービス層のメソッドでinsertを試み例外を発生させる。
public User createUser(CreateUser createUser) {
User user = new User();
user.setId(createUser.getId());
user.setName(createUser.getName());
userDao.insert(user);
return user;
}
このとき、標準エラーには以下が出力される。
〜略〜
nested exception is org.springframework.dao.DuplicateKeyException: [DOMA2004] 一意制約違反により更新処理が失敗しました。
SQLファイルパス=[null]。
ログ用SQL=[]。
詳しい原因は次のものです。{2}; nested exception is org.seasar.doma.jdbc.UniqueConstraintException: [DOMA2004] 一意制約違反により更新処理が失敗しました。
SQLファイルパス=[null]。
ログ用SQL=[]。
詳しい原因は次のものです。{2}] with root cause
java.sql.SQLIntegrityConstraintViolationException: Duplicate entry 'A01' for key 'PRIMARY'
〜略〜
Daoレイヤーから出力されたDuplicateKeyException
がそのままcreateUser
メソッドを突き抜けてスローされている。
弊害としては以下の通り。
- スタックトレースをたどれば
createuser
メソッドで主キー重複例外が出てるから、USER
のID重複エラーだなと一応わかるが、ひと目ではわかりにくい。 - 上位レイヤーのコントローラ層で、Entityに応じたエラーハンドリング処理をしたい場合、
DuplicateKeyException
が投げられると、どのEntityに対しての例外なのか判別するために、ややこしい文字列解析などする必要が出てくる。
上位レイヤーの例外に置き換えた場合
createUser
メソッドの主な関心事はDBの制約ではなく、User
そのものである。ということでUser
に着目した独自例外を作ってやり、それに翻訳する処理を施す。
独自例外
public class UserDuplicatedIdException extends Exception {
public UserDuplicatedIdException(String message, Throwable cause) {
super(message, cause);
}
}
Primary KeyというDB寄りの言葉は使わず、あくまでUser
のid
というUser
寄りの言葉で例外クラスを作る。
createUser
メソッドは以下のように、DuplicateKeyException
からUserDuplicatedIdException
への翻訳処理を実装する。
public User createUser(CreateUser createUser) throws UserDuplicatedIdException {
User user = new User();
user.setId(createUser.getId());
user.setName(createUser.getName());
try {
userDao.insert(user);
} catch (DuplicateKeyException e) {
throw new UserDuplicatedIdException("Can't create already existing id user.", e);
}
return user;
}
先程の例と同じようにid
重複のUSER
テーブルレコードを挿入しようとすると、以下の標準エラー出力が出力される。
〜略〜
nested exception is com.demo.domain.exceptions.UserDuplicatedIdException: Can't create already existing id user.] with root cause
java.sql.SQLIntegrityConstraintViolationException: Duplicate entry 'A01' for key 'PRIMARY'
〜略〜
createUser
からは、このレイヤーの関心事であるUser
に関する例外UserDuplicatedIdException
が出力される。
利点は先程とは逆で以下の通り。
- 例外クラスを見ただけでどのEntityで何が起こったかがひと目で分かる。
- コントローラ層では、catchした例外クラスに応じて楽に分岐処理を実装できる。
また、障害やデバッグ時にログ調査をする際も、目的の例外クラスでgrepをかけられるので捗る。
例外翻訳は乱用しない
先述の例で、USER
のid
をNull
で登録しようとすると、以下の標準エラーが出力される。
〜略〜
nested exception is org.springframework.dao.DataIntegrityViolationException: [DOMA2009] SQLの実行に失敗しました。
SQLファイルパス=[null]。
ログ用SQL=[]。
原因は次のものです。java.sql.SQLIntegrityConstraintViolationException: Column 'id' cannot be null。
根本原因は次のものです。java.sql.SQLIntegrityConstraintViolationException: Column 'id' cannot be null; SQL [insert into USER (id, name) values (?, ?)]; Column 'id' cannot be null; nested exception is java.sql.SQLIntegrityConstraintViolationException: Column 'id' cannot be null] with root cause
java.sql.SQLIntegrityConstraintViolationException: Column 'id' cannot be null
〜略〜
なるほど、この場合はDataIntegrityViolationException
を別の例外に翻訳すればいいんだなと思うかもしれないが、それはNGである。
insert時のDBの中身はクライアント側で制御できないが、insertしようとするEntityは制御可能である。
コントローラ層でリクエストのid
指定がない場合はバリデーションで弾くなり、代替値を入れるなりすれば、この例外は100%発生しないので、翻訳処理も必要なくなる。