springhibernatespring-data-jpatransactionsspring-orm

How to make Hibernate and Spring track transactions consistently?


When using TransactionTemplate in an Executor thread, it appears unable to consistently find the current transaction, resulting in problems like this:

14:20:56.022 [pool-2-thread-1] DEBUG HibernateTransactionManager - Creating new transaction with name [null]: PROPAGATION_REQUIRED,ISOLATION_SERIALIZABLE
14:20:56.022 [pool-2-thread-1] DEBUG HibernateTransactionManager - Opened new Session [SessionImpl(1516598186<open>)] for Hibernate transaction
14:20:56.022 [pool-2-thread-1] DEBUG HibernateTransactionManager - Preparing JDBC Connection of Hibernate Session [SessionImpl(1516598186<open>)]
14:20:56.023 [pool-2-thread-1] DEBUG HibernateTransactionManager - Exposing Hibernate transaction as JDBC [org.springframework.orm.hibernate5.HibernateTransactionManager$$Lambda$1238/0x00000008009dfc40@5cc5f4]
14:20:56.026 [pool-2-thread-1] DEBUG HibernateTransactionManager - Found thread-bound Session [SessionImpl(1516598186<open>)] for Hibernate transaction
14:20:56.026 [pool-2-thread-1] DEBUG HibernateTransactionManager - Participating in existing transaction
14:20:56.052 [pool-2-thread-1] DEBUG SessionFactoryUtils - Flushing Hibernate Session on transaction synchronization
14:20:56.054 [pool-2-thread-1] DEBUG HibernateTransactionManager - Initiating transaction rollback after commit exception
javax.persistence.TransactionRequiredException: no transaction is in progress
    at org.hibernate.internal.AbstractSharedSessionContract.checkTransactionNeededForUpdateOperation(AbstractSharedSessionContract.java:445)
    at org.hibernate.internal.SessionImpl.checkTransactionNeededForUpdateOperation(SessionImpl.java:3478)
    at org.hibernate.internal.SessionImpl.doFlush(SessionImpl.java:1394)
    at org.hibernate.internal.SessionImpl.flush(SessionImpl.java:1389)
    at org.springframework.orm.hibernate5.SessionFactoryUtils.flush(SessionFactoryUtils.java:113)
    at org.springframework.orm.hibernate5.SpringSessionSynchronization.beforeCommit(SpringSessionSynchronization.java:95)
    at org.springframework.transaction.support.TransactionSynchronizationUtils.triggerBeforeCommit(TransactionSynchronizationUtils.java:97)
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.triggerBeforeCommit(AbstractPlatformTransactionManager.java:916)
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:727)
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:711)
    at org.springframework.transaction.support.TransactionTemplate.execute(TransactionTemplate.java:152)
    at org.springframework.transaction.support.TransactionOperations.executeWithoutResult(TransactionOperations.java:67)
    at com.example.MyClass.doProcessing(MyClass.java:109)
    at com.example.MyClass.lambda$scheduleJob$4(MyClass.java:93)
    at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
    at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
    at java.base/java.lang.Thread.run(Thread.java:829)
14:20:56.054 [pool-2-thread-1] DEBUG HibernateTransactionManager - Rolling back Hibernate transaction on Session [SessionImpl(1516598186<open>)]
14:20:56.056 [pool-2-thread-1] DEBUG HibernateTransactionManager - Closing Hibernate Session [SessionImpl(1516598186<open>)] after transaction

JPA is configured manually, because I have multiple DataSources in the application. Relevant beans:

@Bean
@TelemetryDb
public LocalContainerEntityManagerFactoryBean telemetryEntityManagerFactory(
        EntityManagerFactoryBuilder builder,
        @TelemetryDb DataSource dataSource
) {
    return builder.dataSource(dataSource)
            .packages("com.example.model.telemetry")
            .persistenceUnit("telemetry")
            .properties(getVendorProperties(dataSource))
            .jta(false)
            .build();
}

@Bean
@TelemetryDb
@Order(Ordered.HIGHEST_PRECEDENCE)
public PlatformTransactionManager telemetryTransactionManager(@TelemetryDb EntityManagerFactory factory) {
    return new HibernateTransactionManager((SessionFactory) factory);
}

@Configuration
@EnableJpaRepositories(
        basePackageClasses = PackageMarker.class,
        includeFilters = @ComponentScan.Filter(type = FilterType.CUSTOM, value = TelemetryFilter.class),
        entityManagerFactoryRef = "telemetryEntityManagerFactory",
        transactionManagerRef = "telemetryTransactionManager"
)
public static class Telemetry { }

The properties are:

hibernate.auto_quote_keyword=true
hibernate.current_session_context_class=org.springframework.orm.hibernate5.SpringSessionContext
hibernate.dialect=com.example.PostgreSQL12Dialect

And some code that gives the above log:

@Service
@RequriedArgsConstructor
public class MyClass {

    private final MyRepository repository;

    private ExecutorService executor = Executors.newSingleThreadExecutor();
    private SessionFactory sessionFactory;
    private TransactionTemplate transactionTemplate;

    @Autowired
    public void setTransactionManager(@TelemetryDb PlatformTransactionManager manager) {
        this.sessionFactory = ((HibernateTransactionManager) manager).getSessionFactory();
        this.transactionTemplate = new TransactionTemplate(manager);
        this.transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE);
    }

    public scheduleJob(long entityId) {
        Boolean submit = transactionTemplate.execute(status -> {
            MyEntity entity = repository.findById(entity).orElseThrow();
            if (entity.isProcessing()) {
                return false;
            } else {
                entity.setProcessing(true);
                return true;
            }
        });
        if (Boolean.TRUE.equals(submit)) {
            executor.submit(() -> doProcessing(entityId));
        }
    }

    protected void doProcessing(long entityId) {
        transactionTemplate.executeWithoutResult(status -> {
            MyEntity entity = repository.findById(entity).orElseThrow();
            entity.setBlobField(sessionFactory.getCurrentSession().getLobHelper().createBlob(new byte[]{});
            entity.setProcessing(false);
            status.flush();
        });
    }

}

Note that the usage of TransactionTemplate outside of the executor thread completes with no issues. Note also that the createBlob() and flush() do not complain about a missing transaction.

I assume I have missed something in the configuration, but I have been trying various permutations of it, and the doProcessing method, but cannot get anything to work.


After further debugging, it appears that the getCurrentSession() call causes SpringSessionContext to look for, and then create, a session using a SessionFactoryImpl as the key rather than the LocalContainerEntityManagerFactoryBean proxy.

There doesn't seem to be any way to control the construction of the context within Hibernate. I think if I could obtain the raw SessionFactoryImpl to pass to HibernateTransactionManager then that would fix it.


I tried that (using EntityManagerFactory.unrwap) but that broke a whole load of other stuff, because when Spring Data calls createQuery it looks for the current session using the bean proxy.


Solution

  • It appears that SpringSessionContext doesn't work as intended. Either there was a change in Hibernate and/or there wasn't sufficient integration testing.

    There is a workaround. Ignore SessionFactory.getCurrentSession() and use the TransactionSynchronizationManager directly:

    @PersistenceUnit(unitName = "telemetry")
    private EntityManagerFactory entityManagerFactory;
    
    private @NonNull Session getCurrentSession() {
        Object resource = TransactionSynchronizationManager.getResource(entityManagerFactory);
        if (resource == null) {
            throw new IllegalStateException("No current transaction");
        }
        return ((SessionHolder) resource).getSession();
    }
    

    That only works within an existing session. More code would be required to create a new one to match the full behaviour.

    Unsetting hibernate.current_session_context_class ensures you will always get an exception when trying to use the incompatible interface, rather than getting surprise extra sessions.


    Another (simpler) workaround:

    @PersistenceUnit(unitName = "telemetry")
    private EntityManager entityManager;
    
    private @NonNull Session getCurrentSession() {
        return entityManager.unwrap(Session.class);
    }