Search code examples
javaspring-data-jpamulti-tenantspring-boot-3hibernate-6.x

Hibernate multitenancy fails after upgrade to 6.2


I am upgrading a Spring Boot project from 2.7 to 3.1.5, which involves upgrading Hibernate 5 to 6.2. This has totally broken my schema-based multitenancy setup. I am using Postgres 12 along with Spring Data JPA, with various entity classes and JpaRepository interfaces. I test with Testcontainers.

My schema-based multitenancy setup uses implementations of MultiTenantConnectionProvider and CurrentTenantIdentifierResolver, which are @Component Beans. After the upgrade, I am only able to get tests involving a repository and Testcontainers to pass if I do not register the connection provider with Hibernate properties (when running in isolation); however, that makes it impossible for the project to build (with Maven).

In Spring Boot 2.7, I had to make both my multitenancy components implement HibernatePropertiesCustomizer and register themselves with Hibernate properties through AvailableSettings.MULTI_TENANT_ ... (and I'm aware that the MULTI_TENANT type property itself was removed in Hibernate 6). This was the only way I was able to get the app to recognise the components for multitenancy. In Spring Boot 3.1.5 with Hibernate 6.2, the TenantIdentifierResolver registers itself fine - it's only the connection provider that causes tests to fail. But if I don't register it, my app can't build at all, and I have no multitenancy setup.

The error when I register the connection provider:

    org.springframework.transaction.CannotCreateTransactionException: Could not open JPA EntityManager for transaction
 at org.springframework.orm.jpa.JpaTransactionManager.doBegin(JpaTransactionManager.java:466)
 at org.springframework.transaction.support.AbstractPlatformTransactionManager.startTransaction(AbstractPlatformTransactionManager.java:400)
 at org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction(AbstractPlatformTransactionManager.java:373)
 at org.springframework.transaction.interceptor.TransactionAspectSupport.createTransactionIfNecessary(TransactionAspectSupport.java:601)
 at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:385)
 at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
 at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
 at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:137)
 at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
 at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:164)
 at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
 at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)
 at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
 at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:244)
 at jdk.proxy2/jdk.proxy2.$Proxy228.save(Unknown Source)
 at org.fake.pckge.name.TestContainerTests.saveEntityToRepository(TestContainerTests.java:36)
 at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
 at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
Caused by: org.hibernate.TransactionException: JDBC begin transaction failed: 
 at org.hibernate.resource.jdbc.internal.AbstractLogicalConnectionImplementor.begin(AbstractLogicalConnectionImplementor.java:78)
 at org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl.begin(LogicalConnectionManagedImpl.java:282)
 at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl$TransactionDriverControlImpl.begin(JdbcResourceLocalTransactionCoordinatorImpl.java:232)
 at org.hibernate.engine.transaction.internal.TransactionImpl.begin(TransactionImpl.java:83)
 at org.springframework.orm.jpa.vendor.HibernateJpaDialect.beginTransaction(HibernateJpaDialect.java:164)
 at org.springframework.orm.jpa.JpaTransactionManager.doBegin(JpaTransactionManager.java:420)
 ... 85 more
Caused by: java.sql.SQLException: Connection is closed

I have tried registering the connection provider in other ways, such as in a HibernateConfig Bean, or in MultiTenancyJpaConfiguration as in the suggestion from this post, as well as in application properties. Wherever I register it, I have the same issue. I am wondering if it’s a chicken-and-egg situation, where the instantiation of the Beans is too slow for the connection to be established with the right schema. One other piece of the puzzle is that my TenantBasedConnectionProvider relies on a custom @Configuration Bean which loads some properties from the application YAML, such as the names of schemata to use. However, like I say, this worked fine in Spring Boot 2.7 and should be possible, given Hibernate properties are also set in the application YAML.

What can I do?


Solution

  • I have solved it. It was a red herring. While debugging I had introduced the @Container annotation in my PostgresIntegrationTest interface that all Testcontainer tests implement. This annotation closes the Testcontainer after the first test runs. You cannot use @Container or @ServiceConnection - you have to use @DynamicPropertyRegistry and call container.start() within the method

    Another issue was that in MultiTenantConnectionProvider.getConnection I had put the connection inside a try block’s parenthesis, which meant the connection would get closed automatically. You have to leave it open and allow Hibernate to manage it