CannotCreateTransactionException in Multi-Tenant Spring Boot App After Upgrading to Spring Boot 3 and Hibernate 6

212 views Asked by At

I have recently performed an upgrade of my multitenancy Spring Boot application and am now encountering issues with making requests to the database. The upgrade was as follows:

  • Spring Boot 2.5.6 to 3.1.4
  • Java 11 to 17
  • Gradle 7.2 to 8.2.1
  • Hibernate 5 to 6

I am using MariaDB 11.4.4 and Liquibase.

My application starts without any issues after the upgrade. However, every time I try to make a request to the repository that needs to connect to the database, I encounter a CannotCreateTransactionException error. The database is running without any issues and I have confirmed that all the dependencies are correctly installed. Before I upgraded, the application was working as expected. The error that I am encountering now seems to happen every time I attempt to make a database request using the request code snippet provided above.

I get these error messages:

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)

Caused by: java.lang.UnsupportedOperationException: The application must supply JDBC connections

These are the logs from my app:

//...
INFO HHH000204: Processing PersistenceUnitInfo [name: default]
INFO HHH000412: Hibernate ORM core version 6.2.9.Final
INFO HHH000406: Using bytecode reflection optimizer
INFO HHH000021: Bytecode provider name : bytebuddy
INFO No LoadTimeWeaver setup: ignoring JPA class transformer
WARN HHH000181: No appropriate connection provider encountered, assuming application will be supplying connections
WARN HHH000342: Could not obtain connection to query metadata
java.lang.UnsupportedOperationException: The application must supply JDBC connections
    at org.hibernate.engine.jdbc.connections.internal.UserSuppliedConnectionProviderImpl.getConnection(UserSuppliedConnectionProviderImpl.java:44)
    at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator$ConnectionProviderJdbcConnectionAccess.obtainConnection(JdbcEnvironmentInitiator.java:316)
//...
WARN class org.hibernate.tool.schema.Action cannot be cast to class java.lang.String (org.hibernate.tool.schema.Action is in unnamed module of loader org.springframework.boot.loader.LaunchedURLClassLoader @5ce65a89; java.lang.String is in module java.base of loader 'bootstrap')  Ignoring
INFO HHH000021: Bytecode provider name : bytebuddy
INFO HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
INFO Initialized JPA EntityManagerFactory for persistence unit 'default'
INFO Hibernate is in classpath; If applicable, HQL parser will be used.
INFO Tomcat started on port(s): 8080 
INFO Started X in ...
//...
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 jdk.proxy2/jdk.proxy2.$Proxy143.findById(Unknown Source)
    at com......ProjectParamsServiceImpl.getProjectParam(ProjectParamsServiceImpl.java:65)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
//...
    at com......ProjectParamsControllerImpl.getProjectParam(ProjectParamsControllerImpl.java:48)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
//...
    at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.base/java.lang.Thread.run(Thread.java:833)
Caused by: java.lang.UnsupportedOperationException: The application must supply JDBC connections
    at org.hibernate.engine.jdbc.connections.internal.UserSuppliedConnectionProviderImpl.getConnection(UserSuppliedConnectionProviderImpl.java:44)
    at org.hibernate.internal.NonContextualJdbcConnectionAccess.obtainConnection(NonContextualJdbcConnectionAccess.java:38)
    at org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl.acquireConnectionIfNeeded(LogicalConnectionManagedImpl.java:113)
    at org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl.getPhysicalConnection(LogicalConnectionManagedImpl.java:143)
    at org.springframework.orm.jpa.vendor.HibernateJpaDialect.beginTransaction(HibernateJpaDialect.java:152)
    at org.springframework.orm.jpa.JpaTransactionManager.doBegin(JpaTransactionManager.java:420)

This is the part of the code where I get CannotCreateTransactionException error: projectParamsRepository.findById(paramName).orElse(null);

@Service
public class ProjectParamsServiceImpl implements ProjectParamsService {
  @Autowired
  private ProjectParamsRepository projectParamsRepository;

  @Override
  public ProjectParam getProjectParam(String paramName) {
    ProjectParam projectParam = projectParamsRepository.findById(paramName).orElse(null);
  
    //...
  }
}

@Repository
public interface ProjectParamsRepository
        extends JpaRepository<ProjectParam, String>,
        JpaSpecificationExecutor<ProjectParam>,
        ProjectParamsMxRepository {
}

@Entity
@Table(name = "project_param")
public class ProjectParam {
    @Id
    @Column(name = "name")
    @Size(max = 60)
    String name;

    @Lob
    @Column(name = "value")
    String value;
//...
}

Here are my current dependencies from build.gradle:

dependencies {
    implementation 'org.springframework.boot:spring-boot-configuration-processor'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-validation'

    //...

    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'

    implementation 'org.liquibase:liquibase-core:4.19.0'
    runtimeOnly 'mysql:mysql-connector-java:8.0.33'
    liquibase 'org.liquibase.ext:liquibase-hibernate6:4.19.0'
}

This is my application.yml

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    database: mysql
    generate-ddl: true
  liquibase:
    change-log: db/changelog/db.changelog-master.yml

I implemented multitenancy in my Spring Boot app using the classes listed below.

Here's the code for my MultiTenancyJpaConfiguration. I had to comment out three lines in this class because the import import org.hibernate.MultiTenancyStrategy; for them no longer exist.

@Configuration
@EnableConfigurationProperties({JpaProperties.class})
@EnableTransactionManagement
public class MultiTenancyJpaConfiguration {

    @Autowired
    private JpaProperties jpaProperties;

    @Bean
    public MultiTenantConnectionProvider multiTenantConnectionProvider() {
        return new DataSourceBasedMultiTenantConnectionProviderImpl();
    }

    @Bean
    public CurrentTenantIdentifierResolver currentTenantIdentifierResolver() {
        return new CurrentProjectIdentifierResolverImpl();
    }

    @Primary
    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(
            MultiTenantConnectionProvider multiTenantConnectionProvider,
            CurrentTenantIdentifierResolver currentTenantIdentifierResolver
    ) {
        LocalContainerEntityManagerFactoryBean result = new LocalContainerEntityManagerFactoryBean();

        result.setPackagesToScan(
                "com...entity",
                "com...repository",
                "com...repository.impl");
        result.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
        result.setJpaPropertyMap(hibernateProps(multiTenantConnectionProvider, currentTenantIdentifierResolver));

        return result;
    }

    Map<String, Object> hibernateProps(MultiTenantConnectionProvider multiTenantConnectionProvider, CurrentTenantIdentifierResolver currentTenantIdentifierResolver) {
        Map<String, Object> hibernateProps = new LinkedHashMap<>();
        hibernateProps.putAll(this.jpaProperties.getProperties());
//        hibernateProps.put(Environment.MULTI_TENANT, MultiTenancyStrategy.DATABASE);
//        hibernateProps.put(Environment.MULTI_TENANT_CONNECTION_PROVIDER, multiTenantConnectionProvider);
//        hibernateProps.put(Environment.MULTI_TENANT_IDENTIFIER_RESOLVER, currentTenantIdentifierResolver);

        hibernateProps.put(Environment.HBM2DDL_AUTO, Action.NONE);
        hibernateProps.put(Environment.FORMAT_SQL, true);
        hibernateProps.put(Environment.SHOW_SQL, false);

        hibernateProps.put(Environment.DIALECT, "org.hibernate.dialect.MySQLDialect");

        return hibernateProps;
    }

    @Bean
    public EntityManagerFactory entityManagerFactory(LocalContainerEntityManagerFactoryBean entityManagerFactoryBean) {
        return entityManagerFactoryBean.getObject();
    }

    @Bean
    public JpaTransactionManager transactionManager(
            EntityManagerFactory entityManagerFactory
    ) {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(entityManagerFactory);
        return transactionManager;
    }
}

Additionally, this is my CurrentTenantIdentifierResolver class:

@Component
public class CurrentProjectIdentifierResolverImpl implements CurrentTenantIdentifierResolver {

    @Value("${defaultProject.defaultProjectId}")
    String defaultProjectId;

    @Override
    public String resolveCurrentTenantIdentifier() {
        String project = UserProjectContent.getCurrentProject(); //ThreadLocal
        if (StringUtils.isNotBlank(project))
            return project;

        return defaultProjectId;
    }

    @Override
    public boolean validateExistingCurrentSessions() {
        return true;
    }

}

Here is my AbstractDataSourceBasedMultiTenantConnectionProviderImpl class:

@Component
public class DataSourceBasedMultiTenantConnectionProviderImpl
        extends AbstractDataSourceBasedMultiTenantConnectionProviderImpl {
    private static final long serialVersionUID = 1L;

    @Autowired
    private ProjectDatabaseRepository projectDatabaseInfoRepository;

    @Autowired
    private ProjectRepository projectRepository;

    @Value("${refreshIntervalForWaitingProjectsInSeconds}")
    Integer refreshIntervalForWaitingProjectsInSeconds;

    @Value("${defaultProject.defaultProjectId}")
    String defaultProjectId;

    private final Map<String, DataSource> dataSourcesMtApp = new TreeMap<>();

    @Override
    protected DataSource selectAnyDataSource() {
        if (dataSourcesMtApp.isEmpty()) {
            List<ProjectDbInfo> projects = new ArrayList<>();
            //...
            addProjects(projects);
        }
        return this.dataSourcesMtApp.values().iterator().next();
    }

    @Override
    protected DataSource selectDataSource(String projectId) {
        if (!this.dataSourcesMtApp.containsKey(projectId)) {
            ProjectDbInfo project = projectDatabaseInfoRepository.getByProjectId(projectId);
            addProject(project);
        }
        return this.dataSourcesMtApp.get(projectId);
    }

    public void addProjects(List<ProjectDbInfo> projects) {
        for (ProjectDbInfo project : projects) {
            addProject(project);
        }
    }

    public void addProject(ProjectDbInfo project) {
        dataSourcesMtApp.put(
                project.getId(),
                DataSourceUtil.createAndConfigureDataSource(project)
        );
    }
}

This is my DataSourceUtil class: an example of a masterProject.getUrl() is: jdbc:mysql://mariadb:3306/examples?createDatabaseIfNotExist=true&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC

public final class DataSourceUtil {

    public static DataSource createAndConfigureDataSource(ProjectDbInfo masterProject) {
        HikariDataSource ds = new HikariDataSource();
        ds.setUsername(masterProject.getUsername());
        ds.setPassword(masterProject.getPassword());
        ds.setJdbcUrl(masterProject.getUrl());
        ds.setConnectionTimeout(20000);

        ds.setMinimumIdle(0);
        ds.setMaximumPoolSize(1);
        ds.setIdleTimeout(30000);
        ds.setMaxLifetime(30000);
        String projectId = masterProject.getId();
        String projectConnectionPoolName = projectId + "-connection-pool";
        ds.setPoolName(projectConnectionPoolName);
        return ds;
    }
}

I believe that the error is associated with the upgrade from Hibernate 5 to 6, in conjunction with changes made in the MultiTenancyJpaConfiguration class.

Given this information, could someone please help me figure out why I am getting a CannotCreateTransactionException here and how I can fix this problem? Any suggestions would be much appreciated.

Update: Solution

I solved the problem by making the following changes in the MultiTenancyJpaConfiguration class.

@Configuration
public class MultiTenancyJpaConfiguration {

    @Primary
    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSourceBasedMultiTenantConnectionProviderImpl multiTenantConnectionProvider,
                                                                       CurrentProjectIdentifierResolverImpl tenantIdentifierResolver) {
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();

        em.setPackagesToScan(
                "com....entity",
                "com....repository",
                "com....repository.impl");

        em.setJpaVendorAdapter(new HibernateJpaVendorAdapter());

        Map<String, Object> jpaProperties = new HashMap<>();
        jpaProperties.put(Environment.MULTI_TENANT_CONNECTION_PROVIDER, multiTenantConnectionProvider);
        jpaProperties.put(Environment.MULTI_TENANT_IDENTIFIER_RESOLVER, tenantIdentifierResolver);
        jpaProperties.put(Environment.FORMAT_SQL, true);

        jpaProperties.put("hibernate.hbm2ddl.auto", "none");
        jpaProperties.put("hibernate.format_sql", true);
        jpaProperties.put("hibernate.show_sql", false);
        jpaProperties.put(Environment.PHYSICAL_NAMING_STRATEGY, PhysicalNamingStrategyStandardImpl.class);
        jpaProperties.put(Environment.DIALECT, "org.hibernate.dialect.MySQLDialect");

        em.setJpaPropertyMap(jpaProperties);

        return em;
    }

    @Bean(name = "transactionManager")
    public PlatformTransactionManager transactionManager(LocalContainerEntityManagerFactoryBean entityManagerFactoryBean) {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(entityManagerFactoryBean.getObject());
        return transactionManager;
    }
}

I hope this helps anyone who encounters a similar problem after upgrading to Spring Boot 3 and Hibernate 6 with a multi-tenant configuration.

0

There are 0 answers