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?
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()
:
Maintenant, si ServiceUser.method()
n’est pas transactionnel, voici ce qui se passe:
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:
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.
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
).
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
).
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
).
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.
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.
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
).
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 .
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".
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 .
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";
}