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
to3.1.4
- Java
11
to17
- Gradle
7.2
to8.2.1
- Hibernate
5
to6
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.