Hibernate/Spring 引导:如何从回滚中排除实体

Hibernate/Spring Boot: How to exclude an entity from rollback

描述

发生异常时排除 Hibernate 托管实体在事务中回滚的最佳方法是什么?

我有一个由 Spring Batch 调用的服务。 Spring 批量打开并提交一个我无法真正控制的事务。在服务过程中,某些实体被更新。但是,当发生异常时,它会传播到调用者并且所有处理工作都会回滚。这正是我想要的,除了一个特定的实体,该实体额外用于跟踪流程状态以便于监控和其他原因。如果出现错误,我想更新该实体以显示错误状态(在服务 class 内)。

例子

我试图通过打开一个单独的事务并提交更新来实现这一点:

@Service
@Transactional
public class ProcessService {

    @Autowired
    private EntityManagerFactory emf;

    @Autowired
    private ProcessEntryRepository repository;

    @Override
    public void process(ProcessEntry entry) {          
        try {
            entry.setStatus(ProcessStatus.WORK_IN_PROGRESS);
            entry.setTimeWorkInProgress(LocalDateTime.now());
            entry = repository.saveAndFlush(entry);
            createOrUpdateEntities(entry.getData()); // should all be rolled back in case of exception
            entry.setStatus(ProcessStatus.FINISHED);
            entry.setTimeFinished(LocalDateTime.now());
            repository.saveAndFlush(entry);
        } catch (Exception e) {
            handleException(entry, e); // <- does not work as expected
            throw e; // hard requirement to rethrow here
        }
    }

    private void handleException(ProcessEntry entry, Exception exception) {
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

        StatelessSession statelessSession = null;
        Transaction transaction = null;
        try {
            statelessSession = openStatelessSession();
            transaction = statelessSession.beginTransaction();
            
            ProcessEntry entryUnwrap = unwrapHibernateProxy(entry); // to get actual Class

            @SuppressWarnings("unchecked")
            ProcessEntry entryInSession = (ProcessEntry) statelessSession.get(entryUnwrap.getClass(), entryUnwrap.getId());
            entryInSession.setStatus(ProcessStatus.FAILED);
            entryInSession.setTimeFailed(LocalDateTime.now());
            entryInSession.setStatusMessage(exception.getMessage());
            save(transaction, entryInSession);
            commit(statelessSession, transaction);
        catch (Exception e) {
            LOGGER.error("Exception occurred while setting FAILED status on: " + entry, e);
            rollback(transaction);
        } finally {
            closeStatelessSession(statelessSession);
        }
    }

    public StatelessSession openStatelessSession() {
        EntityManagerFactory entityManagerFactory = emf;
        if (emf instanceof MultipleEntityManagerFactorySwitcher) {
            entityManagerFactory = ((MultipleEntityManagerFactorySwitcher) emf).currentFactory();
        }
        return ((HibernateEntityManagerFactory) entityManagerFactory).getSessionFactory()
        .openStatelessSession();
    }

    public static Long save(StatelessSession statelessSession, ProcessEntry entity) {
        if (!entity.isPersisted()) {
            return (Long) statelessSession.insert(entity.getClass().getName(), entity);
        }

        statelessSession.update(entity.getClass().getName(), entity);
        return entity.getId();
    }

    public static void commit(StatelessSession statelessSession, Transaction transaction) {
        ((TransactionContext) statelessSession).managedFlush();
        transaction.commit();
    }

    public static void rollback(Transaction transaction) {
        if (transaction != null) {
            transaction.rollback();
        }
    }

    public static void closeStatelessSession(StatelessSession statelessSession) {
        if (statelessSession != null) {
            statelessSession.close();
        }
    }

}

然而,由于某种原因,这种方法在 save -> statelessSession.update(..) 上完全冻结了应用程序(或者更确切地说,批处理作业永远不会完成)! 请注意,已知方法 openStatelessSessionsaverollbackcommit 等可以在其他上下文中使用。我将它们复制到这个例子中。 我也尝试使用 repository 来加载实体,但在这里我当然得到 StaleObjectException.

我还尝试使用 @Transaction(Propagation.REQUIRES_NEW) 来获得一个单独的异常处理程序服务,但它会保留实体管理器中的所有更改,而不仅仅是 ProcessEntry 实体。您可以事先清除会话,但在我的情况下也确实冻结了。

这很可能是因为您冻结的是数据库,而不是您的应用程序。您正在尝试插入一条记录,而该记录在另一个事务完成之前就已尝试插入该记录。这很可能将数据库从行锁变为完全 table 锁。所以每个事务都在等待另一个事务关闭,然后才能继续。

您可以尝试其中之一:

1.  Fully roll back and complete the first transaction before trying to redo the insert.   Since you are still in the exception processing logic holding the exception from being thrown Spring Batch will not have rolled back the transaction for that step yet.

2.  Try to do the the extra insert in one of Spring Batch's onError() listeners.

3.  Do the insert into a separate temp table (you can create as the start of the job, and delete at the end of the job.) and use an onComplete() listener for the step to then copy any entries in that temp table to your intended table.