springの @Transactional
を使用するとメソッドの外に例外が伝播した際にロールバックしてくれます。
ただし(デフォルトでは)プロキシ経由の外部メソッド呼び出しのみが対象になるということで、所謂 @Autowired
したインスタンスから呼び出すメソッドに @Transactional
を付与する必要があります。
そこで以下のようなコードを組んだのですが、これは期待した通りの動きにはなりませんでした。
@Service
class ServiceA {
@Transactional
public void updateEntities(List<EntityA> entities) {
// 全ての要素に対して UPDATE を実行して欲しい
// エラーが起きても後続は処理を続ける
for (Entity entity : entities) {
try {
update(entity)
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Transactional
private void update(EntityA entity) {
int cnt = dao.updateRecord(entity);
if (cnt != 1) {
// 更新件数が 1 で無ければ例外を投げてロールバックしたい
throw new RuntimeException();
}
}
}
別のクラスから updateEntities()
を呼び出し「全要素に対して処理を行いつつ、1 要素に対して更新件数が 1 で無ければその要素はロールバックしたい」という処理です。
これのテストを記述して試してみたところ、update()
が例外を投げてもコミットされており、ロールバックされていませんでした。
主に間違っている点としては 2 つ
1. 内部呼び出しを行なっている
冒頭にもさらっと記載した通り、@Transactional
に働いてもらうにはデフォルトでは別のクラスから呼ばれている必要があります。
今回 update()
は同一クラスの updateEntities()
から呼び出されてしまっています。
2. 可視性が private になっている
可視性は public である必要があるそうです(6.0 以降は protected または package private も可能とのこと)。
別クラスに切り出して対処
要素のカタマリを操作するクラスと単一の要素を操作するクラスに分けてしまおうという発想です。
クラスを分けてしまえば外部呼び出しの形になるので期待通りの動作となります。
また、ドキュメントを読んだだけで試していませんが mode を aspectj に変更し、設定を修正すれば自己呼び出しも対応可能なようです。
class ServiceA {
public void updateEntities(List<EntityA> entities) {
for (Entity entity : entities) {
try {
serviceB.update(entity)
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
class ServiceB {
@Transactional
private void update(EntityA entity) {
int cnt = dao.updateRecord(entity);
if (cnt != 1) {
throw new RuntimeException();
}
}
}
教訓:ドキュメントを読む