我们的大多数 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)
);
// ...
}
// ```
}
我查看了
@TransactionScoped
beans,但是 @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(...)
方法返回实体而不是其 id。该实体附加到持久性上下文。
现在,资源方法不再需要
@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
属性中。