Quarkus: обновлять таблицу базы данных, даже если внешняя транзакция откатилась

Большинство наших конечных точек Quarkus следуют стандартной практике, согласно которой они помечаются знаком @Transactional, вызывают наш бизнес-уровень, и если выдается исключение, вся транзакция откатывается.

Однако в этом сценарии нам необходимо выполнить обновление базы данных, даже если транзакция отменена. Наша база данных — MySQL 8.

// Quarkus Resource class
@POST
@Transactional
@Path("/document/generate")
public void generateDocument() {
    documentGeneratorComponent.generateDocument(...);
}

Наша первоначальная попытка заключалась в том, чтобы использовать @Transactional(REQUIRES_NEW) для обновления статуса. Проблема, с которой мы сталкиваемся, заключается в том, что мы получаем исключения тайм-аута блокировки, поскольку я считаю, что и внешняя транзакция, и вложенная транзакция пытаются обновить одну и ту же запись отслеживания.

@ApplicationScoped
public class DocumentGeneratorComponent {


    public void generateDocument(...) {
        Long trackingId = null;
        try {
            trackingId = createTrackingRecord(DocGenStatus.STARTED);

            // error prone stuff that may throw
            var input = getInputData(...);
            docGenService.sendDocRequest(input);
        } catch (Exception ex) {
            if (trackingId != null) {
                updateStatusWithError(trackingId);
            }
            throw ex;
        }
    }

    // updates tracking record even if error
    @Transactional(REQUIRES_NEW)
    public void updateStatusWithError(var trackingId) {
        updateTrackingRecord(trackingId, DocGenStatus.EXCEPTION);
    }
}

Изначально мы думали, что можем удалить аннотацию @Transactional со слоя ресурсов и обрабатывать транзакцию в компоненте. Проблема в том, что другому коду на нашем бизнес-уровне также может потребоваться генерировать документы, и они могут делать это в рамках своей транзакции.

Было бы невероятно удобно, если бы существовал простой способ выполнения кода в обратном вызове, подобный следующему. Есть ли лучший способ сделать подобные вещи в Quarkus?

public void generateDocument(...) {
    Long trackingId = null;
    try {
    } finally {
       transactionManager.onCurrentTransactionRollback(
            () -> updateStatusWithError(trackingId)
        );
    // ...
    }
// ```
}

Я изучил bean-компоненты @TransactionScoped, но задокументировано, что @PreDestroy вызывается до отката транзакции, и существует открытый дефект, из-за которого кажется, что поведение при выполнении не определено или не соответствует документации.

Существуют также прослушиватели транзакций, но их использование кажется немного неудобным, поскольку они прослушивают все транзакции, а не конкретную.

void onAfterEndTransaction(@Observes @Destroyed(TransactionScoped.class) Object event) {
  // will be invoked for every transaction in the application, not just the code in question
}

Каков рекомендуемый подход? Спасибо!

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
0
87
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

JPA имеет возможность контролировать исключения, при которых транзакции необходимо откатить. Аннотация @Transactional имеет два атрибута: rollbackOn и dontRollbackOn.

Согласно документации:

Элемент dontRollbackOn может быть установлен для указания исключений, которые не должны приводить к тому, что перехватчик помечает транзакцию для отката. И наоборот, элемент rollbackOn может быть установлен для указания исключений, которые должны заставить перехватчик пометить транзакцию для отката. Если для любого из этих элементов указан класс, назначенное поведение применяется также к подклассам этого класса. Если указаны оба элемента, dontRollbackOn имеет приоритет.

В моем решении нет необходимости запускать транзакцию на уровне ресурсов, но она также работает с другими транзакциями.

@ApplicationScoped
public class DocumentGeneratorComponent {

    @Transactional(dontRollbackOn = DocumentException.class)
    public void generateDocument() {

        var trackingRecord = createTrackingRecord(DocGenStatus.STARTED);
        try {
            var input = getInputData(trackingRecord.id);
            // ...

            // It was missing in the question, but I assume it would be helpful 
            // to mark when the document generation finished successfully.
            trackingRecord.docGenStatus = DocGenStatus.FINISHED;
        } catch (DocumentException e) {
            trackingRecord.docGenStatus = DocGenStatus.EXCEPTION;
            throw e;
        }
    }

    @Transactional
    TrackingRecord createTrackingRecord(DocGenStatus status) {
        TrackingRecord record = new TrackingRecord();
        record.docGenStatus = status;
        record.persistAndFlush();
        return record;
    }

    String getInputDate(Long trackingRecordId) {
        if (null == trackingRecordId) {
            throw new DocumentException("Invalid tracking record (null).");
        }
        if (trackingRecordId % 2 == 0L) {
            return "FOO";
        }
        throw new DocumentException("Sometimes it is happen.");
    }

}

Пример сущности TrackingRecord:

@Entity
public class TrackingRecord extends PanacheEntity {
    @Enumerated(EnumType.STRING)
    public DocGenStatus docGenStatus;
}

Как видите, метод createTrackingRecord(...) возвращает сущность вместо ее идентификатора. Этот объект привязан к контексту сохранения.

Теперь аннотация @Transactional для метода ресурса больше не требуется.

@Path("/document")
public class DocumentResource {
    
    @Inject
    DocumentGeneratorComponent documentGeneratorComponent;

    @POST
    @Path("/generate")
    public void generateDocument() {
        documentGeneratorComponent.generateDocument();
    }
}

Хорошо, а как насчет других бизнес-услуг, которые

им также может потребоваться создать документы, и они могут сделать это в рамках своей транзакции.

Это также возможно.

@ApplicationScoped
public class OtherBusinessService {

    @Inject
    DocumentGeneratorComponent documentGeneratorComponent;

    @Transactional(value = Transactional.TxType.REQUIRES_NEW)
    public void whateverBusinessMethod() {
        var entity = new CustomBusinessEntity();
        entity.name = "John Doe";
        entity.persist();
        try {
            documentGeneratorComponent.generateDocument();
            entity.documentGenerated = true;
        } catch (DocumentException e) {
            entity.documentGenerated = false;
        }
    }
}

OtherBusinessService.whateverBusinessMethod() сохранится как для CustomBusinessEntity, так и для TrackingRecord сущностей.

Важно то, что OtherBusinessService или любой другой бизнес-метод, который обрабатывает собственную транзакцию, должен обрабатывать это определенное исключение следующими способами:

  • Перехватывайте исключение во внешнем бизнес-методе и не выдавайте его повторно или
  • если необходим повторный бросок, добавьте это исключение во внешний атрибут @Transactional аннотации dontRollbackOn.

спасибо, что так много думали об этом ответе. Как-то я этого раньше не видел. Думаю, мысленно я застрял на фразе: «Я хочу выполнить откат, а затем выполнить очистку после отката». Это казалось более простой ментальной моделью. Итак, хотя другой ответ был больше похож на то, о чем я думал, похоже, мне следовало принять именно этот подход. Я предполагаю, что важным моментом является то, что вызывающая сторона, OtherBusinessService, должна обернуть вызов в try/catch, иначе исключение всплывет, и транзакция будет отменена, независимо от того, правильно ли я ее читаю. Спасибо!

nogridbag 12.06.2024 02:39

Вы исследовали использование синхронизации транзакций? Метод обратного вызова Synchronization.afterCompletion будет вызван после завершения транзакции, поэтому блокировки базы данных будут сняты.

Спасибо, это казалось именно тем, что я искал.

nogridbag 12.06.2024 02:18

Другие вопросы по теме