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?
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:
- l'intercepteur transactionnel intercepte l'appel de méthode, et démarre une transaction, parce qu'aucune transaction n'est déjà active
- la méthode s'appelle
- la méthode appelle MyService.doSth ()
- l'intercepteur transactionnel intercepte l'appel de méthode, voit qu'une transaction est déjà active, et ne fait rien
- doSth () est exécuté et lance une exception
- le transactionnel intercepteur intercepte l'exception des marques, la transaction comme rollbackOnly, et se propage à l'exception
- ServiceUser.la méthode () capture l'exception et renvoie
- 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:
- la méthode s'appelle
- la méthode appelle MyService.doSth ()
- l'intercepteur transactionnel intercepte l'appel de méthode, voit qu'aucune transaction n'est déjà active, et commence ainsi une transaction
- doSth () est exécuté et lance une exception
- 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
- ServiceUser.la méthode () capture l'exception et renvoie
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:
Transaction marquée pour le renvoi par exception rejetée de
method2
. C'est notre cas par défaut expliqué par JB Nizet.Annotation
method2
@Transactional(readOnly = true)
marque toujours le mouvement pour le retour (exception lancée lors de la sortie demethod1
).annotant les deux
method1
etmethod2
@Transactional(readOnly = true)
marque toujours le mouvement pour le retour (exception lancée lors de la sortie demethod1
).Annotation
method2
@Transactional(noRollbackFor = SomeException)
empêche de marquer le mouvement pour un retour en arrière (pas d'exception jetés à la sortie demethod1
).Supposons que
method2
appartientService1
. L'appelant depuismethod1
ne passe pas par le printemps proxy, i.e. Spring n'est pas au courant deSomeException
jetés demethod2
. La Transaction est pas marqué pour la restauration dans ce cas.Supposons que
method2
n'est pas annoté avec@Transactional
. L'appelant depuismethod1
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.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 demethod2
mais le mouvement original n'est pas affecté dans ce cas (pas d'exception jetés à la sortie demethod1
).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 sortiemethod1
).
Voir tous les scénarios testés dans ce gist.
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.
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";
}
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."