Подтвердить что ты не робот

Не удалось выполнить транзакцию JPA: транзакция, отмеченная как rollbackOnly

Я использую Spring и Hibernate в одном из приложений, над которым я работаю, и у меня возникла проблема с обработкой транзакций.

У меня есть класс обслуживания, который загружает некоторые объекты из базы данных, изменяет некоторые из их значений, а затем (когда все действительно) совершает эти изменения в базе данных. Если новые значения недействительны (которые я могу проверить только после их установки), я не хочу сохранять изменения. Чтобы предотвратить Spring/Hibernate от сохранения изменений, я вызываю исключение в методе. Это приводит к следующей ошибке:

Could not commit JPA transaction: Transaction marked as rollbackOnly

И это сервис:

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

  }
}

И вот как я его вызываю:

class ServiceUser {
  @Autowired
  private MyService myService;

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

То, что я ожидал бы: Никаких изменений в базе данных и никаких исключений, видимых для пользователя.

Что происходит: никаких изменений в базе данных, но приложение сбой:

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

Он правильно устанавливает транзакцию на rollbackOnly, но почему сбой отката с исключением?

4b9b3361

Ответ 1

Я предполагаю, что ServiceUser.method() сам является транзакционным. Этого не должно быть. Вот почему.

Здесь, когда происходит вызов метода ServiceUser.method():

  • транзакционный перехватчик перехватывает вызов метода и запускает транзакцию, поскольку транзакция уже не активна.
  • метод называется
  • метод вызывает MyService.doSth()
  • транзакционный перехватчик перехватывает вызов метода, видит, что транзакция уже активна и ничего не делает
  • doSth() выполняется и генерирует исключение
  • транзакционный перехватчик перехватывает исключение, отмечает транзакцию как rollbackOnly и распространяет исключение
  • ServiceUser.method() получает исключение и возвращает
  • транзакционный перехватчик, так как он начал транзакцию, пытается его совершить. Но Hibernate отказывается это делать, потому что транзакция отмечена как rollbackOnly, поэтому Hibernate выдает исключение. Перехватчик транзакций передает его вызывающему абоненту, бросая исключение, обертывающее исключение спящего режима.

Теперь, если ServiceUser.method() не является транзакционным, вот что происходит:

  • метод называется
  • метод вызывает MyService.doSth()
  • транзакционный перехватчик перехватывает вызов метода, видит, что транзакция уже не активна и, таким образом, запускает транзакцию
  • doSth() выполняется и генерирует исключение
  • транзакционный перехватчик перехватывает исключение. Так как он запустил транзакцию, и после того, как было выбрано исключение, он откатывает транзакцию и распространяет исключение.
  • ServiceUser.method() получает исключение и возвращает

Ответ 2

Не удалось выполнить транзакцию JPA: транзакция, помеченная как rollbackOnly

Это исключение возникает, когда вы вызываете вложенные методы/службы, также помеченные как @Transactional. JB Nizet подробно объяснил механизм. Я хотел бы добавить некоторые сценарии, когда это произойдет, а также некоторые способы избежать этого.

Предположим, что у нас есть две службы Spring: Service1 и Service2. Из нашей программы мы вызываем Service1.method1(), который, в свою очередь, вызывает Service2.method2():

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

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

SomeException не проверяется (расширяет исключение RuntimeException), если не указано иное.

Сценарии:

  • Транзакция, помеченная для отката исключением, исключенным из method2. Это наш случай по умолчанию, описанный JB Nizet.

  • Аннотирование method2 как @Transactional(readOnly = true) по-прежнему отмечает транзакцию для отката (исключение выбрано при выходе из method1).

  • Аннотирование как method1, так и method2 как @Transactional(readOnly = true) по-прежнему отмечает транзакцию для отката (исключение выбрано при выходе из method1).

  • Аннотирование method2 с помощью @Transactional(noRollbackFor = SomeException) предотвращает маркировку транзакции для отката ( исключение, выведенное при выходе из method1).

  • Предположим, что method2 принадлежит Service1. Вызов из method1 не проходит через прокси-сервер Spring, т.е. Spring не знает о SomeException, выброшенном из method2. Транзакция не отмечена для отката в этом случае.

  • Предположим, что method2 не аннотируется с @Transactional. Вызов из method1 проходит через прокси-сервер Spring, но Spring не обращает внимания на выброшенные исключения. Транзакция не отмечена для отката в этом случае.

  • Аннотирование method2 с помощью @Transactional(propagation = Propagation.REQUIRES_NEW) делает method2 запуск новой транзакции. Вторая транзакция отмечена для отката при выходе из method2, но исходная транзакция не затрагивается в этом случае ( исключение, выведенное при выходе из method1).

  • В случае проверки SomeException (не распространяется на RuntimeException), Spring по умолчанию не помещает транзакцию для отката при перехвате отмеченных исключений ( исключение, выведенное при выходе из method1).

Посмотрите все сценарии, протестированные в этот метод.

Ответ 3

Как объяснил @Ярослав Ставничий, если служба помечена как транзакционная транзакция spring пытается обрабатывать транзакцию. Если возникает какое-либо исключение, выполняется операция отката. Если в вашем сценарии ServiceUser.method() не выполняет транзакционную операцию, вы можете использовать аннотацию @Transactional.TxType. Опция "НИКОГДА" используется для управления этим методом вне транзакционного контекста.

Ссылка на транзакцию .TxType здесь.

Ответ 4

Сначала сохраните вспомогательный объект, а затем вызовите метод сохранения окончательного репозитория.

@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";
    }