web-dev-qa-db-ja.com

JPAトランザクションをコミットできませんでした:rollbackOnlyとしてマークされたトランザクション

作業中のアプリケーションの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に正しく設定していますが、例外でロールバックがクラッシュするのはなぜですか?

33
user3346601

私の推測では、ServiceUser.method()はそれ自体がトランザクションです。すべきではありません。これが理由です。

ServiceUser.method()メソッドが呼び出されると、次のようになります。

  1. トランザクションインターセプターはメソッド呼び出しをインターセプトし、トランザクションは既にアクティブになっていないため、トランザクションを開始します
  2. メソッドが呼び出されます
  3. メソッドはMyService.doSth()を呼び出します
  4. トランザクションインターセプターはメソッド呼び出しをインターセプトし、トランザクションが既にアクティブであることを確認し、何もしません
  5. doSth()が実行され、例外がスローされます
  6. トランザクションインターセプターは例外をインターセプトし、トランザクションをrollbackOnlyとしてマークし、例外を伝播します
  7. ServiceUser.method()は例外をキャッチして戻ります
  8. トランザクションインターセプターはトランザクションを開始したため、コミットを試みます。ただし、トランザクションがrollbackOnlyとしてマークされているため、Hibernateはそれを拒否し、Hibernateは例外をスローします。トランザクションインターセプターは、休止状態の例外をラップする例外をスローすることにより、呼び出し元に通知します。

ServiceUser.method()がトランザクションに対応していない場合、次のようになります。

  1. メソッドが呼び出されます
  2. メソッドはMyService.doSth()を呼び出します
  3. トランザクションインターセプターはメソッド呼び出しをインターセプトし、トランザクションがまだアクティブでないことを確認して、トランザクションを開始します
  4. doSth()が実行され、例外がスローされます
  5. トランザクションインターセプターは例外をインターセプトします。トランザクションを開始し、例外がスローされたため、トランザクションをロールバックし、例外を伝播します
  6. ServiceUser.method()は例外をキャッチして戻ります
54
JB Nizet

JPAトランザクションをコミットできませんでした:トランザクションはrollbackOnlyとしてマークされています

この例外は、@Transactionalとしてもマークされているネストされたメソッド/サービスを呼び出すときに発生します。 JB Nizetがメカニズムを詳細に説明しました。いくつかのシナリオが発生した場合と、いくつかの回避方法を追加したい

Service1Service2の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を拡張します)。

シナリオ:

  1. method2からスローされた例外により、ロールバック対象としてマークされたトランザクション。これは、JB Nizetが説明したデフォルトのケースです。

  2. method2@Transactional(readOnly = true)として注釈を付けると、トランザクションにロールバックのマークが付けられます(method1を終了するときに例外がスローされます)。

  3. method1method2の両方に@Transactional(readOnly = true)として注釈を付けると、トランザクションがロールバックとしてマークされます(method1を終了するときに例外がスローされます)。

  4. method2@Transactional(noRollbackFor = SomeException)の注釈を付けると、トランザクションをロールバックにマークできなくなります(例外なしmethod1を終了するときにスローされます)。

  5. method2Service1に属しているとします。 method1から呼び出すと、Springのプロキシを経由しません。つまり、Springはmethod2からスローされるSomeExceptionを認識しません。この場合、トランザクションはロールバック用にマークされていません

  6. method2@Transactionalの注釈が付いていないとします。 method1から呼び出すと、Springのプロキシを経由しますが、Springはスローされた例外に注意を払いません。この場合、トランザクションはロールバック用にマークされていません

  7. method2@Transactional(propagation = Propagation.REQUIRES_NEW)アノテーションを付けると、method2が新しいトランザクションを開始します。その2番目のトランザクションはmethod2の終了時にロールバックのマークが付けられますが、この場合、元のトランザクションは影響を受けません( no exception method1を終了するときにスローされます)。

  8. SomeExceptionchecked(RuntimeExceptionを拡張しない)の場合、Springはデフォルトでチェック例外をインターセプトするときにトランザクションをロールバックにマークしません( no exception method1を終了するときにスローされます。

this Gist でテストされたすべてのシナリオを参照してください。

18

Rollback-flagが設定される原因となった元の例外を追跡するデバッガーをセットアップできない(またはしたくない)場合は、コード全体にデバッグステートメントの束を追加して行を見つけることができますロールバック専用フラグをトリガーするコードの例:

logger.debug("Is rollbackOnly: " + TransactionAspectSupport.currentTransactionStatus().isRollbackOnly());

これをコード全体に追加することで、デバッグステートメントに番号を付け、上記のメソッドが「false」から「true」を返す場所を確認することで、根本原因を絞り込むことができました。

1

最初にサブオブジェクトを保存してから、最後のリポジトリ保存メソッドを呼び出します。

@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";
    }
1
Nirbhay Rana

@Yaroslav Stavnichiyが説明したように、サービスがトランザクションスプリングとしてマークされている場合、トランザクション自体を処理しようとします。例外が発生すると、ロールバック操作が実行されます。シナリオでServiceUser.method()がトランザクション操作を実行していない場合は、@ Transactional.TxTypeアノテーションを使用できます。 「NEVER」オプションは、トランザクションコンテキスト外でそのメソッドを管理するために使用されます。

Transactional.TxType参照ドキュメントは here です。

0
mahkras