升级到 Spring Boot 3 和 Hibernate 6 后,多租户 Spring Boot 应用程序中出现 CannotCreateTransactionException

问题描述 投票:0回答:1

我最近对我的

multitenancy Spring Boot
应用程序进行了升级,现在在向数据库发出请求时遇到了问题。升级如下:

  • Spring Boot
    2.5.6
    3.1.4
  • Java
    11
    17
  • 摇篮
    7.2
    8.2.1
  • 休眠
    5
    6

我正在使用 MariaDB

11.4.4
和 Liquibase。

我的应用程序在升级后启动没有任何问题。但是,每次尝试向需要连接数据库的存储库发出请求时,都会遇到

CannotCreateTransactionException
错误。数据库正在运行,没有任何问题,并且我已确认所有依赖项均已正确安装。在升级之前,该应用程序按预期运行。我现在遇到的错误似乎每次我尝试使用上面提供的请求代码片段发出数据库请求时都会发生。

我收到这些错误消息:

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

这些是我的应用程序的日志:

//...
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)

这是代码中我收到

CannotCreateTransactionException
错误的部分:
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;
//...
}

这是我当前的依赖项:

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'
}

这是我的

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

我使用下面列出的类在 Spring Boot 应用程序中实现了多租户。

这是我的

MultiTenancyJpaConfiguration
的代码。 我必须注释掉此类中的三行,因为它们的导入
import org.hibernate.MultiTenancyStrategy;
不再存在。

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

另外,这是我的

CurrentTenantIdentifierResolver
课程:

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

}

这是我的

AbstractDataSourceBasedMultiTenantConnectionProviderImpl
课程:

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

这是我的

DataSourceUtil
课程:
masterProject.getUrl()
的一个示例是:
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;
    }
}

我认为该错误与

upgrade from Hibernate 5 to 6
以及
MultiTenancyJpaConfiguration
类中所做的更改有关。

鉴于此信息,有人可以帮我弄清楚为什么我在这里得到

CannotCreateTransactionException
以及如何解决这个问题吗?任何建议将不胜感激。

更新:解决方案

我通过在

MultiTenancyJpaConfiguration
类中进行以下更改解决了该问题。

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

我希望这可以帮助任何在使用多租户配置升级到 Spring Boot 3 和 Hibernate 6 后遇到类似问题的人。

java spring-boot multi-tenant spring-boot-3 hibernate-6.x
1个回答
0
投票

我有一个非常相似的问题。将 SB 2.7 -> 3.1.5 和 Hibernate 5 升级到 6.2。我使用基于模式的多租户设置,并实现了

MultiTenantConnectionProvider
CurrentTenantIdentifierResolver
,它们是
@Component
Bean。升级后,如果我不向 Hibernate 注册连接提供程序,我只能通过涉及存储库/测试容器的测试 - 但是,这使得无法构建(使用 Maven)。

我的两个 Hibernate 组件都实现

HibernatePropertiesCustomizer
并通过
AvailableSettings.MULTI_TENANT ...
向 Hibernate 注册自己 (我知道
MULTI_TENANT
类型属性在 Hibernate 6 中已被删除)。

TenantIdentifierResolver
本身注册得很好 - 只是连接提供程序失败了。我曾尝试将其注册在
HibernateConfig
Bean 或
MultiTenancyJpaConfiguration
中,如您的示例(以及应用程序属性),但无论我在哪里注册它,我都会遇到同样的问题。如果我注册它,Testcontainer 测试将永远无法创建与数据库的连接。如果我不注册,我的应用程序将无法构建。

如果我注册了连接提供商:

    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

我认为这是一个先有鸡还是先有蛋的情况,但在升级之前效果很好。另一个难题是我的

TenantBasedConnectionProvider
依赖于自定义
@Configuration
Bean,它从应用程序 YAML 加载一些属性,例如要使用的模式名称。

如果有人对同一主题有一些见解,我将不胜感激。另一点是我正在使用 Postgres。

© www.soinside.com 2019 - 2024. All rights reserved.