Большинство наших конечных точек 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
}
Каков рекомендуемый подход? Спасибо!
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
.Вы исследовали использование синхронизации транзакций? Метод обратного вызова Synchronization.afterCompletion будет вызван после завершения транзакции, поэтому блокировки базы данных будут сняты.
Спасибо, это казалось именно тем, что я искал.
спасибо, что так много думали об этом ответе. Как-то я этого раньше не видел. Думаю, мысленно я застрял на фразе: «Я хочу выполнить откат, а затем выполнить очистку после отката». Это казалось более простой ментальной моделью. Итак, хотя другой ответ был больше похож на то, о чем я думал, похоже, мне следовало принять именно этот подход. Я предполагаю, что важным моментом является то, что вызывающая сторона, OtherBusinessService, должна обернуть вызов в try/catch, иначе исключение всплывет, и транзакция будет отменена, независимо от того, правильно ли я ее читаю. Спасибо!