web-dev-qa-db-fra.com

Impossible de commettre une transaction JPA: transaction marquée en tant que 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 à partir de la base de données, modifie certaines de leurs valeurs, puis valide (lorsque tout est valide) ces modifications dans la base de données. Si les nouvelles valeurs ne sont pas valides (ce que je ne peux vérifier qu'après les avoir définies), je ne souhaite pas conserver les modifications. Pour empêcher Spring/Hibernate de sauvegarder les modifications, je jette une exception dans la méthode. Cela entraîne toutefois l'erreur suivante: 

Could not commit JPA transaction: Transaction marked as rollbackOnly

Et voici 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 voici comment je l'invoque:

class ServiceUser {
  @Autowired
  private MyService myService;

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

Ce à quoi je m'attendais: aucune modification de la base de données et aucune exception visible pour l'utilisateur.

Que se passe-t-il: aucune modification de 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

La transaction est correctement définie sur rollbackOnly, mais pourquoi l'annulation échoue-t-elle avec une exception?

30
user3346601

Je suppose que ServiceUser.method() est lui-même une transaction. Cela ne devrait pas être. Voici la raison pour laquelle.

Voici ce qui se passe lorsqu'un appel est fait à votre méthode ServiceUser.method():

  1. l'intercepteur transactionnel intercepte l'appel de méthode et démarre une transaction, car 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 lève une exception
  6. l'intercepteur transactionnel intercepte l'exception, marque la transaction comme étant rollbackOnly et propage l'exception
  7. ServiceUser.method () intercepte l'exception et renvoie
  8. l'intercepteur transactionnel, depuis qu'il a démarré la transaction, tente de la commettre. Mais Hibernate refuse de le faire car la transaction est marquée comme étant rollbackOnly. Hibernate lève donc une exception. L'intercepteur de transaction le signale à l'appelant en lançant une exception qui enveloppe 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 lance ainsi une transaction
  4. doSth () est exécuté et lève une exception
  5. l'intercepteur transactionnel intercepte l'exception. Depuis qu’elle a démarré la transaction et qu’une exception a été levée, elle annule la transaction et propage l’exception.
  6. ServiceUser.method () intercepte l'exception et renvoie
48
JB Nizet

Impossible de commettre une transaction JPA: transaction marquée en tant que rollbackOnly

Cette exception se produit lorsque vous appelez des méthodes/services imbriqués également marqués en tant que @Transactional. JB Nizet a expliqué le mécanisme en détail. J'aimerais ajouter quelques scénarios quand cela se produit ainsi que quelques moyens de l'éviter.

Supposons que nous ayons deux services Spring: Service1 et Service2. Depuis 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 n'est pas cochée (étend RuntimeException) sauf indication contraire.

Scénarios:

  1. La transaction marquée pour une annulation par une exception jetée hors de method2. C'est notre cas par défaut expliqué par JB Nizet.

  2. L'annotation de method2 en tant que @Transactional(readOnly = true) marque toujours la transaction pour l'annulation (exception levée lors de la sortie de method1).

  3. L'annotation de method1 et method2 en tant que @Transactional(readOnly = true) marque toujours la transaction pour l'annulation (exception levée lors de la sortie de method1).

  4. L'annotation de method2 avec @Transactional(noRollbackFor = SomeException) empêche de marquer la transaction pour l'annulation (no exception levée lors de la sortie de method1).

  5. Supposons que method2 appartient à Service1. L’appel depuis method1 ne passe pas par le proxy de Spring, c’est-à-dire que Spring n’a pas connaissance du fait que SomeException a été éjecté de method2. La transaction est non marquée pour l'annulation dans ce cas.

  6. Supposons que method2 ne soit pas annoté avec @Transactional. L'appeler à partir de method1 passe par le proxy de Spring, mais Spring ne fait pas attention aux exceptions levées. La transaction est non marquée pour l'annulation dans ce cas.

  7. En annotant method2 avec @Transactional(propagation = Propagation.REQUIRES_NEW), method2 commence une nouvelle transaction. Cette seconde transaction est marquée pour être annulée lors de la sortie de method2 mais la transaction d'origine n'est pas affectée dans ce cas (no exception levée lors de la sortie de method1).

  8. Dans le cas où SomeException vaut vérifié (ne prolonge pas l'exception RuntimeException), Spring par défaut ne marque pas la transaction pour l'annulation lors de l'interception des exceptions vérifiées (no exception levée lors de la sortie de method1).

Voir tous les scénarios testés dans this Gist .

16

Pour ceux qui ne peuvent pas (ou ne veulent pas) configurer un débogueur pour localiser l'exception d'origine qui causait l'activation de l'indicateur d'annulation, vous pouvez simplement ajouter un tas d'instructions de débogage dans votre code pour trouver les lignes. de code qui déclenche l'indicateur d'annulation uniquement:

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

Ajouter tout au long du code m'a permis de circonscrire la cause première, en numérotant les instructions de débogage et en cherchant à voir où la méthode ci-dessus ne renvoie pas "false" à "true".

1
Joel Richard Koett

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

Le document de référence Transactional.TxType est ici .

0
mahkras

Enregistrez d’abord le sous-objet, puis appelez la méthode de sauvegarde du référentiel final.

@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
Nirbhay Rana