简体   繁体   中英

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

Description

What is the best way to exclude a Hibernate managed entity from being rolled back in a transaction when an exception occurs?

I have a service that is called by Spring Batch. Spring Batch opens and commits a transaction which I don't really have control over. In the service process certain entities are updated. However when an exception occurs it is propagated to the caller and all the processing work is rolled back. That is exactly what I want except for a specific entity that is additionally used to track the process state for easier monitoring and other reasons. In case of an error I want to update that entity to display the error status (within the service class).

Example

I've tried to achieve this by opening a separate transaction and committing the update:

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

}

However this approach freezes up the application entirely (or rather the batch job never finishes) on save -> statelessSession.update(..) for some reason! Note that the methods openStatelessSession , save , rollback , commit etc. are known to work in other contexts. I copied them into this example. I also tried working with repository to load the entity but here of course I get StaleObjectException .

I also played around with @Transaction(Propagation.REQUIRES_NEW) for a separate exception handler service but that persists all the changes in the entity manager and not only the ProcessEntry entity.

It is very likely since you freezing the database, not your app. You are trying to insert a record that is already trying to be inserted in another transaction before the other transaction is completed. Which is likley taking the database from a row lock to a full table lock. So each transaction is waiting on the other to close before they can move forward.

You can try one of these:

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM