Transaction JPA: Transaction marquée comme rollbackOnly

J'utilise Spring et Hibernate dans l'une des applications sur lesquelles je travaille et j'ai un problème avec le traitement des transactions.

j'ai une classe de service qui charge certaines entités de la base de données, modifie certaines de leurs valeurs et ensuite (quand tout est valide) envoie ces modifications à la base de données. Si les nouvelles valeurs sont invalides (que je ne peux vérifier qu'après les avoir paramétrées) Je ne veux pas persister dans les changements. Pour empêcher le printemps / L'hibernation de sauver les changements I faites une exception à la méthode. Il en résulte toutefois l'erreur suivante:

Could not commit JPA transaction: Transaction marked as rollbackOnly

Et c'est le service:

@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();
    }

  }
}

Et c'est ainsi que j'invoque:

class ServiceUser {
  @Autowired
  private MyService myService;

  public void method() {
    try {
      myService.doSth();
    } catch (MyCustomException e) {
      // ...
    }        
  }
}

ce à quoi je m'attendais: aucun changement dans la base de données et aucune exception visible pour l'utilisateur.

Ce qui se passe: Pas de changements à la base de données, mais l'application se bloque avec:

org.springframework.transaction.TransactionSystemException: Could not commit JPA transaction;
nested exception is javax.persistence.RollbackException: Transaction marked as rollbackOnly

il est correctement le réglage de la transaction à rollbackOnly mais pourquoi est le rollback un accident avec une exception?

26
demandé sur Joel Richard Koett 2014-08-15 11:21:59

5 réponses

a mon avis, c'est ServiceUser.method() est lui-même transactionnel. Il ne devrait pas l'être. Voici la raison pourquoi.

voici ce qui se passe quand un appel est fait à votre ServiceUser.method() méthode:

  1. l'intercepteur transactionnel intercepte l'appel de méthode, et démarre une transaction, parce qu'aucune transaction n'est déjà active
  2. la méthode s'appelle
  3. la méthode appelle MyService.doSth ()
  4. l'intercepteur transactionnel intercepte l'appel de méthode, voit qu'une transaction est déjà active, et ne fait rien
  5. doSth () est exécuté et lance une exception
  6. le transactionnel intercepteur intercepte l'exception des marques, la transaction comme rollbackOnly, et se propage à l'exception
  7. ServiceUser.la méthode () capture l'exception et renvoie
  8. transactionnelle de l'intercepteur, depuis qu'il a commencé la transaction, tente de la commettre. Mais Hibernate refuse de le faire parce que la transaction est marqué comme rollbackOnly, donc Hibernate fait une exception. L'intercepteur de transaction le signale à l'appelant en lançant une exception enveloppant l'exception d'hibernation.

Maintenant, si ServiceUser.method() n'est pas transactionnel, voici ce qui se passe:

  1. la méthode s'appelle
  2. la méthode appelle MyService.doSth ()
  3. l'intercepteur transactionnel intercepte l'appel de méthode, voit qu'aucune transaction n'est déjà active, et commence ainsi une transaction
  4. doSth () est exécuté et lance une exception
  5. l'intercepteur transactionnel intercepte l'exception. Depuis qu'il a commencé la transaction, et depuis qu'une exception a été lancée, il retrousse la transaction, et propage l'exception
  6. ServiceUser.la méthode () capture l'exception et renvoie
43
répondu JB Nizet 2014-08-15 08:07:59

ne pouvait pas commettre de transaction JPA: Transaction marquée comme rollbackOnly

cette exception se produit lorsque vous invoquez des méthodes/services imbriqués également marqués comme @Transactional. JB Nizet a expliqué le mécanisme en détail. Je voudrais ajouter quelques scénarios quand il arrive ainsi que certains façons de l'éviter.

supposons que nous ayons deux services de printemps:Service1 et Service2. De notre programme nous appelons Service1.method1() qui à son tour appelle Service2.method2():

class Service1 {
    @Transactional
    public void method1() {
        try {
            ...
            service2.method2();
            ...
        } catch (Exception e) {
            ...
        }
    }
}

class Service2 {
    @Transactional
    public void method2() {
        ...
        throw new SomeException();
        ...
    }
}

SomeException est non vérifié (étend RuntimeException) sauf indication contraire.

Scénarios:

  1. Transaction marquée pour le renvoi par exception rejetée de method2. C'est notre cas par défaut expliqué par JB Nizet.

  2. Annotation method2@Transactional(readOnly = true) marque toujours le mouvement pour le retour (exception lancée lors de la sortie de method1).

  3. annotant les deux method1 et method2@Transactional(readOnly = true) marque toujours le mouvement pour le retour (exception lancée lors de la sortie de method1).

  4. Annotation method2@Transactional(noRollbackFor = SomeException) empêche de marquer le mouvement pour un retour en arrière (pas d'exception jetés à la sortie de method1).

  5. Supposons que method2 appartient Service1. L'appelant depuis method1 ne passe pas par le printemps proxy, i.e. Spring n'est pas au courant de SomeException jetés de method2. La Transaction est pas marqué pour la restauration dans ce cas.

  6. Supposons que method2 n'est pas annoté avec @Transactional. L'appelant depuis method1 passe par le proxy de Spring, mais Spring ne fait pas attention aux exceptions lancées. La Transaction est pas marqué pour la restauration dans ce cas.

  7. Annotation method2@Transactional(propagation = Propagation.REQUIRES_NEW)method2 démarrer une nouvelle transaction. Cette seconde transaction est marquée pour un retour en arrière lors de la sortie de method2 mais le mouvement original n'est pas affecté dans ce cas (pas d'exception jetés à la sortie de method1).

  8. au cas où SomeException coché (ne prolonge pas RuntimeException), Spring par défaut ne marque pas la transaction pour le retour lors de l'interception des exceptions vérifiées (pas d'exception jetés à la sortie method1).

Voir tous les scénarios testés dans ce gist.

13
répondu Yaroslav Stavnichiy 2017-03-20 15:21:26

comme expliqué @Yaroslav Stavnichiy si un service est marqué comme ressort transactionnel tente de gérer la transaction elle-même. Si une exception se produit alors une opération de retour effectué. Si dans votre scénario, ServiceUser.la méthode () n'effectue aucune opération transactionnelle que vous pouvez utiliser @Transactional.Annotation TxType. L'option "Jamais" est utilisée pour gérer cette méthode en dehors du contexte transactionnel.

transactionnel.TxType document de référence est ici.

0
répondu mahkras 2017-11-16 16:32:56

Enregistrer sous-objet premier et ensuite appeler final référentiel méthode de sauvegarde.

@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";
    }
0
répondu Nirbhay Rana 2018-01-03 09:54:29

pour ceux qui ne peuvent pas (ou ne veulent pas) configurer un débogueur pour traquer l'exception d'origine qui causait le déclenchement du drapeau rollback, vous pouvez juste ajouter un tas d'instructions de débogage dans tout votre code pour trouver les lignes de code qui déclenchent le drapeau rollback-only:

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

L'ajout de ceci tout au long du code m'a permis de réduire la cause fondamentale, en numérotant les déclarations de débogage et en regardant pour voir où la méthode ci-dessus va de retourner "false" à "vrai."

0
répondu Joel Richard Koett 2018-07-25 06:20:55