web-dev-qa-db-de.com

JPA-Transaktion konnte nicht festgeschrieben werden: Transaktion als rollbackOnly gekennzeichnet

Ich verwende Spring und Hibernate in einer der Anwendungen, an denen ich gerade arbeite, und ich habe ein Problem mit der Abwicklung von Transaktionen.

Ich habe eine Service-Klasse, die einige Entitäten aus der Datenbank lädt, einige ihrer Werte ändert und dann (wenn alles gültig ist) diese Änderungen in die Datenbank übergibt. Wenn die neuen Werte ungültig sind (was ich nur nach dem Einstellen überprüfen kann), möchte ich die Änderungen nicht beibehalten. Um zu verhindern, dass Spring/Hibernate die Änderungen speichert, löse ich eine Ausnahme in der Methode aus. Dies führt jedoch zu folgendem Fehler: 

Could not commit JPA transaction: Transaction marked as rollbackOnly

Und das ist der 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();
    }

  }
}

Und so rufe ich es an:

class ServiceUser {
  @Autowired
  private MyService myService;

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

Was ich erwarten würde: Keine Änderungen an der Datenbank und keine für den Benutzer sichtbaren Ausnahmen.

Was passiert: Keine Änderungen an der Datenbank, aber die App stürzt ab mit:

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

Sie setzt die Transaktion korrekt auf rollbackOnly, aber warum stürzt das Rollback mit einer Ausnahme ab?

30
user3346601

Meine Vermutung ist, dass ServiceUser.method() selbst transaktional ist. Es sollte nicht sein. Hier ist der Grund warum.

Folgendes passiert, wenn Sie Ihre ServiceUser.method()-Methode aufrufen:

  1. der Transaktionsinterceptor fängt den Methodenaufruf ab und startet eine Transaktion, da noch keine Transaktion aktiv ist
  2. die Methode wird aufgerufen
  3. die Methode ruft MyService.doSth () auf
  4. der Transaktionsinterceptor fängt den Methodenaufruf ab, stellt fest, dass eine Transaktion bereits aktiv ist und nichts tut
  5. doSth () wird ausgeführt und löst eine Ausnahme aus
  6. der Transaktionsinterceptor fängt die Ausnahme ab, markiert die Transaktion als rollbackOnly und gibt die Ausnahme weiter
  7. ServiceUser.method () fängt die Ausnahme ab und gibt zurück
  8. der Transaktions-Interceptor versucht, die Transaktion zu übernehmen, da er die Transaktion gestartet hat. Hibernate lehnt dies jedoch ab, da die Transaktion als rollbackOnly gekennzeichnet ist. Daher gibt Hibernate eine Ausnahme aus. Der Transaktionsinterceptor signalisiert es dem Aufrufer, indem er eine Ausnahme auslöst, die die Ruhezustandsausnahme umgibt.

Wenn ServiceUser.method() keine Transaktion ist, passiert Folgendes:

  1. die Methode wird aufgerufen
  2. die Methode ruft MyService.doSth () auf
  3. der Transaktions-Interceptor fängt den Methodenaufruf ab, erkennt, dass keine Transaktion bereits aktiv ist, und startet eine Transaktion
  4. doSth () wird ausgeführt und löst eine Ausnahme aus
  5. der Transaktionsinterceptor fängt die Ausnahme ab. Da die Transaktion gestartet wurde und eine Ausnahme ausgelöst wurde, wird die Transaktion rückgängig gemacht und die Ausnahme weitergegeben
  6. ServiceUser.method () fängt die Ausnahme ab und gibt zurück
48
JB Nizet

JPA-Transaktion konnte nicht festgeschrieben werden: Transaktion als rollbackOnly gekennzeichnet

Diese Ausnahme tritt auf, wenn Sie verschachtelte Methoden/Dienste aufrufen, die auch als @Transactional markiert sind. JB Nizet erläuterte den Mechanismus im Detail. Ich möchte einige Szenarien hinzufügen, wenn dies passiert, sowie einige Möglichkeiten, dies zu vermeiden.

Angenommen, wir haben zwei Spring-Services: Service1 und Service2. Aus unserem Programm rufen wir Service1.method1() auf, die wiederum Service2.method2() aufruft:

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

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

SomeException ist nicht markiert (erweitert RuntimeException), sofern nicht anders angegeben.

Szenarien:

  1. Transaktion, die durch eine Ausnahme aus method2 für ein Rollback markiert wurde. Dies ist unser Standardfall, der von JB Nizet erläutert wird.

  2. Die Beschriftung von method2 als @Transactional(readOnly = true) markiert immer noch die Transaktion für das Rollback (Ausnahme beim Beenden von method1).

  3. Das Annotieren von method1 und method2 als @Transactional(readOnly = true) markiert immer noch die Transaktion für ein Rollback (Ausnahme beim Beenden von method1).

  4. Durch das Kommentieren von method2 mit @Transactional(noRollbackFor = SomeException) wird das Markieren der Transaktion für ein Rollback verhindert (keine Ausnahme, die beim Beenden von method1 ausgelöst wird).

  5. Angenommen, method2 gehört zu Service1. Das Aufrufen von method1 durchläuft keinen Spring-Proxy, d. H. Spring kennt keine SomeException, die aus method2 verworfen wurde. Die Transaktion ist in diesem Fall für Rollback nicht markiert.

  6. Angenommen, method2 wird nicht mit @Transactional kommentiert. Das Aufrufen von method1 durchläuft Spring's Proxy, aber Spring achtet nicht auf die ausgelösten Ausnahmen. Die Transaktion ist in diesem Fall für Rollback nicht markiert.

  7. Durch das Kommentieren von method2 mit @Transactional(propagation = Propagation.REQUIRES_NEW) wird method2 eine neue Transaktion gestartet. Diese zweite Transaktion wird beim Beenden von method2 für ein Rollback markiert. Die ursprüngliche Transaktion ist in diesem Fall jedoch nicht betroffen (keine Ausnahme, die beim Beenden von method1 ausgelöst wird).

  8. Wenn SomeExceptiongeprüft ist (erweitert RuntimeException nicht), markiert Spring standardmäßig keine Transaktion für das Rollback, wenn geprüfte Ausnahmen abgefangen werden (keine Ausnahme, die beim Beenden von method1 ausgelöst wird).

Alle in this Gist getesteten Szenarien anzeigen.

16

Für diejenigen, die keinen Debugger einrichten können (oder wollen), um die ursprüngliche Ausnahme zu ermitteln, die dazu geführt hat, dass das Rollback-Flag gesetzt wurde, können Sie einfach eine Reihe von Debug-Anweisungen im gesamten Code hinzufügen, um die Zeilen zu finden von Code, der das Nur-Zurücksetzen-Flag auslöst:

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

Wenn Sie dies im gesamten Code hinzufügen, konnte ich die eigentliche Ursache eingrenzen, indem Sie die Debug-Anweisungen nummerieren und nachsehen, wo die obige Methode von "false" auf "true" zurückgeht.

1

@Yaroslav Stavnichiy erklärt, wenn ein Dienst als Transaktionsquelle gekennzeichnet ist, versucht er, die Transaktion selbst auszuführen. Wenn eine Ausnahme auftritt, wird eine Rollback-Operation ausgeführt. Wenn ServiceUser.method () in Ihrem Szenario keine Transaktionsoperation ausführt, können Sie die Annotation @ Transactional.TxType verwenden. Die Option 'NEVER' wird verwendet, um diese Methode außerhalb des Transaktionskontexts zu verwalten. 

Transactional.TxType-Referenzdokument ist hier .

0
mahkras

Speichern Sie zuerst das Unterobjekt und rufen Sie dann die endgültige Repository-Speichermethode auf.

@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