作業中のアプリケーションの1つでSpringとHibernateを使用していますが、トランザクションの処理に問題があります。
データベースからいくつかのエンティティをロードし、それらの値の一部を変更し、(すべてが有効な場合に)これらの変更をデータベースにコミットするサービスクラスがあります。新しい値が無効な場合(設定後にのみ確認できます)、変更を保持したくありません。 Spring/Hibernateが変更を保存するのを防ぐために、メソッドで例外をスローします。ただし、これにより次のエラーが発生します。
Could not commit JPA transaction: Transaction marked as rollbackOnly
そして、これがサービスです:
@Service
class MyService {
@Transactional(rollbackFor = MyCustomException.class)
public void doSth() throws MyCustomException {
//load entities from database
//modify some of their values
//check if they are valid
if(invalid) { //if they arent valid, throw an exception
throw new MyCustomException();
}
}
}
そして、これは私がそれを呼び出す方法です:
class ServiceUser {
@Autowired
private MyService myService;
public void method() {
try {
myService.doSth();
} catch (MyCustomException e) {
// ...
}
}
}
予想されること:データベースへの変更はなく、例外はユーザーに表示されません。
何が起こるか:データベースは変更されませんが、アプリは次のようにクラッシュします:
org.springframework.transaction.TransactionSystemException: Could not commit JPA transaction;
nested exception is javax.persistence.RollbackException: Transaction marked as rollbackOnly
トランザクションをrollbackOnlyに正しく設定していますが、例外でロールバックがクラッシュするのはなぜですか?
私の推測では、ServiceUser.method()
はそれ自体がトランザクションです。すべきではありません。これが理由です。
ServiceUser.method()
メソッドが呼び出されると、次のようになります。
ServiceUser.method()
がトランザクションに対応していない場合、次のようになります。
JPAトランザクションをコミットできませんでした:トランザクションはrollbackOnlyとしてマークされています
この例外は、@Transactional
としてもマークされているネストされたメソッド/サービスを呼び出すときに発生します。 JB Nizetがメカニズムを詳細に説明しました。いくつかのシナリオが発生した場合と、いくつかの回避方法を追加したい。
Service1
とService2
の2つのSpringサービスがあるとします。プログラムからService1.method1()
を呼び出し、次にService2.method2()
を呼び出します:
class Service1 {
@Transactional
public void method1() {
try {
...
service2.method2();
...
} catch (Exception e) {
...
}
}
}
class Service2 {
@Transactional
public void method2() {
...
throw new SomeException();
...
}
}
特に明記しない限り、SomeException
はチェックされません(RuntimeExceptionを拡張します)。
シナリオ:
method2
からスローされた例外により、ロールバック対象としてマークされたトランザクション。これは、JB Nizetが説明したデフォルトのケースです。
method2
に@Transactional(readOnly = true)
として注釈を付けると、トランザクションにロールバックのマークが付けられます(method1
を終了するときに例外がスローされます)。
method1
とmethod2
の両方に@Transactional(readOnly = true)
として注釈を付けると、トランザクションがロールバックとしてマークされます(method1
を終了するときに例外がスローされます)。
method2
に@Transactional(noRollbackFor = SomeException)
の注釈を付けると、トランザクションをロールバックにマークできなくなります(例外なしがmethod1
を終了するときにスローされます)。
method2
がService1
に属しているとします。 method1
から呼び出すと、Springのプロキシを経由しません。つまり、Springはmethod2
からスローされるSomeException
を認識しません。この場合、トランザクションはロールバック用にマークされていません。
method2
に@Transactional
の注釈が付いていないとします。 method1
から呼び出すと、Springのプロキシを経由しますが、Springはスローされた例外に注意を払いません。この場合、トランザクションはロールバック用にマークされていません。
method2
に@Transactional(propagation = Propagation.REQUIRES_NEW)
アノテーションを付けると、method2
が新しいトランザクションを開始します。その2番目のトランザクションはmethod2
の終了時にロールバックのマークが付けられますが、この場合、元のトランザクションは影響を受けません( no exception がmethod1
を終了するときにスローされます)。
SomeException
がchecked(RuntimeExceptionを拡張しない)の場合、Springはデフォルトでチェック例外をインターセプトするときにトランザクションをロールバックにマークしません( no exception method1
を終了するときにスローされます。
this Gist でテストされたすべてのシナリオを参照してください。
Rollback-flagが設定される原因となった元の例外を追跡するデバッガーをセットアップできない(またはしたくない)場合は、コード全体にデバッグステートメントの束を追加して行を見つけることができますロールバック専用フラグをトリガーするコードの例:
logger.debug("Is rollbackOnly: " + TransactionAspectSupport.currentTransactionStatus().isRollbackOnly());
これをコード全体に追加することで、デバッグステートメントに番号を付け、上記のメソッドが「false」から「true」を返す場所を確認することで、根本原因を絞り込むことができました。
最初にサブオブジェクトを保存してから、最後のリポジトリ保存メソッドを呼び出します。
@PostMapping("/save")
public String save(@ModelAttribute("shortcode") @Valid Shortcode shortcode, BindingResult result) {
Shortcode existingShortcode = shortcodeService.findByShortcode(shortcode.getShortcode());
if (existingShortcode != null) {
result.rejectValue(shortcode.getShortcode(), "This shortode is already created.");
}
if (result.hasErrors()) {
return "redirect:/shortcode/create";
}
**shortcode.setUser(userService.findByUsername(shortcode.getUser().getUsername()));**
shortcodeService.save(shortcode);
return "redirect:/shortcode/create?success";
}
@Yaroslav Stavnichiyが説明したように、サービスがトランザクションスプリングとしてマークされている場合、トランザクション自体を処理しようとします。例外が発生すると、ロールバック操作が実行されます。シナリオでServiceUser.method()がトランザクション操作を実行していない場合は、@ Transactional.TxTypeアノテーションを使用できます。 「NEVER」オプションは、トランザクションコンテキスト外でそのメソッドを管理するために使用されます。
Transactional.TxType参照ドキュメントは here です。