在 Hibernate 多租户配置中禁用 Spring 数据源配置

Posted

技术标签:

【中文标题】在 Hibernate 多租户配置中禁用 Spring 数据源配置【英文标题】:Disable Spring datasource configuration in Hibernate multi tenant configuration 【发布时间】:2019-03-14 17:06:40 【问题描述】:

我正在使用 Spring 2.x、Spring Data REST、Hibernate 5.x、mysql 创建一个服务器 REST 应用程序。

我按照以下指南配置了多租户:https://dzone.com/articles/spring-boot-hibernate-multitenancy-implementation,唯一的区别是我为每个租户使用一个数据库。

我有一个MultiTenantConnectionProvider 用于创建与数据库的连接,还有一个TenantIdentifierResolver 用于获取当前租户。

一些相关的代码:

@Component

public class TenantIdentifierResolver implements CurrentTenantIdentifierResolver 

    @Override

    public String resolveCurrentTenantIdentifier() 

        String tenantId = TenantContext.getCurrentTenant();

        if (tenantId != null) 

            return tenantId;

        

        return DEFAULT_TENANT_ID;

    

    @Override

    public boolean validateExistingCurrentSessions() 

        return true;

    


...

@Component
@Profile("prod")
public class MultiTenantConnectionProviderImpl implements MultiTenantConnectionProvider 
    private static final long serialVersionUID = 3193007611085791247L;
    private Logger log = LogManager.getLogger();

    private Map<String, HikariDataSource> dataSourceMap = new ConcurrentHashMap<String, HikariDataSource>();

    @Autowired
    private TenantRestClient tenantRestClient;

    @Autowired
    private PasswordEncrypt passwordEncrypt;

    @Override
    public void releaseAnyConnection(Connection connection) throws SQLException 
        connection.close();
    

    @Override
    public Connection getAnyConnection() throws SQLException 
        Connection connection = getDataSource(TenantIdResolver.TENANT_DEFAULT).getConnection();
        return connection;

    

    @Override
    public Connection getConnection(String tenantId) throws SQLException 
        Connection connection = getDataSource(tenantId).getConnection();
        return connection;
    

    @Override
    public void releaseConnection(String tenantId, Connection connection) throws SQLException 
        log.info("releaseConnection " + tenantId);
        connection.close();
    

    @Override
    public boolean supportsAggressiveRelease() 
        return false;
    

    @Override
    public boolean isUnwrappableAs(Class unwrapType) 
        return false;
    

    @Override
    public <T> T unwrap(Class<T> unwrapType) 
        return null;
    

    public HikariDataSource getDataSource(@NotNull String tentantId) throws SQLException 
        if (dataSourceMap.containsKey(tentantId)) 
            return dataSourceMap.get(tentantId);
         else 
            HikariDataSource dataSource = createDataSource(tentantId);
            dataSourceMap.put(tentantId, dataSource);
            return dataSource;
        
    

    public HikariDataSource createDataSource(String tenantId) throws SQLException 
        log.info("Create Datasource for tenant ", tenantId);
        try 
            Database database = tenantRestClient.getDatabase(tenantId);
            DatabaseInstance databaseInstance = tenantRestClient.getDatabaseInstance(tenantId);
            if (database != null && databaseInstance != null) 
                HikariConfig hikari = new HikariConfig();
                String driver = "";
                String options = "";
                switch (databaseInstance.getType()) 
                case MYSQL:
                    driver = "jdbc:mysql://";
                    options = "?useLegacyDatetimeCode=false&serverTimezone=UTC&useUnicode=yes&characterEncoding=UTF-8&useSSL=false";
                    break;

                default:
                    driver = "jdbc:mysql://";
                    options = "?useLegacyDatetimeCode=false&serverTimezone=UTC&useUnicode=yes&characterEncoding=UTF-8&useSSL=false";
                

                hikari.setJdbcUrl(driver + databaseInstance.getHost() + ":" + databaseInstance.getPort() + "/" + database.getName() + options);
                hikari.setUsername(database.getUsername());
                hikari.setPassword(passwordEncrypt.decryptPassword(database.getPassword()));

                // MySQL optimizations, see
                // https://github.com/brettwooldridge/HikariCP/wiki/MySQL-Configuration
                hikari.addDataSourceProperty("cachePrepStmts", true);
                hikari.addDataSourceProperty("prepStmtCacheSize", "250");
                hikari.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
                hikari.addDataSourceProperty("useServerPrepStmts", "true");
                hikari.addDataSourceProperty("useLocalSessionState", "true");
                hikari.addDataSourceProperty("useLocalTransactionState", "true");
                hikari.addDataSourceProperty("rewriteBatchedStatements", "true");
                hikari.addDataSourceProperty("cacheResultSetMetadata", "true");
                hikari.addDataSourceProperty("cacheServerConfiguration", "true");
                hikari.addDataSourceProperty("elideSetAutoCommits", "true");
                hikari.addDataSourceProperty("maintainTimeStats", "false");
                hikari.setMinimumIdle(3);
                hikari.setMaximumPoolSize(5);

                hikari.setIdleTimeout(30000);
                hikari.setPoolName("JPAHikari_" + tenantId);
                // mysql wait_timeout 600seconds
                hikari.setMaxLifetime(580000);
                hikari.setLeakDetectionThreshold(60 * 1000);

                HikariDataSource dataSource = new HikariDataSource(hikari);


                return dataSource;

             else 
                throw new SQLException(String.format("DB not found for tenant %s!", tenantId));
            
         catch (Exception e) 
            throw new SQLException(e.getMessage());
        
    


我也以这种方式配置了 Hibernate:

@Configuration
@Profile("prod")
public class HibernateConfig 

    @Autowired
    private JpaProperties jpaProperties;

    @Bean
    public JpaVendorAdapter jpaVendorAdapter() 
        return new HibernateJpaVendorAdapter();
    

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource,
            MultiTenantConnectionProvider multiTenantConnectionProviderImpl,
            CurrentTenantIdentifierResolver currentTenantIdentifierResolverImpl) 
        Map<String, Object> properties = new HashMap<>();
        properties.putAll(jpaProperties.getHibernateProperties(new HibernateSettings()));
        properties.put(Environment.MULTI_TENANT, MultiTenancyStrategy.DATABASE);
        properties.put(Environment.MULTI_TENANT_CONNECTION_PROVIDER, multiTenantConnectionProviderImpl);
        properties.put(Environment.MULTI_TENANT_IDENTIFIER_RESOLVER, currentTenantIdentifierResolverImpl);
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(dataSource);
        em.setPackagesToScan("com.server");
        em.setJpaVendorAdapter(jpaVendorAdapter());
        em.setJpaPropertyMap(properties);

        return em;
    


这是我的 application.properties:

spring.datasource.url=jdbc:mysql://url:3306/empty?useLegacyDatetimeCode=false&serverTimezone=UTC&useSSL=false
spring.datasource.username=empty
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.password=empty
  spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL57Dialect
spring.jpa.hibernate.ddl-auto: validate
spring.jpa.hibernate.naming.physical-   strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
spring.jpa.show-sql: false

在应用程序启动期间,我看到 Spring 为我在属性文件中配置的数据库创建了一个连接池。

我想避免这种情况,因为我所有的连接都是由MultiTenantConnectionProviderImpl 创建的。 我想继续在我的 bean 中注入 EntityManagerDatasource

我已经看到如何禁用 Spring Boot 数据源配置 here,但是这样做我无法再在我的应用程序中注入数据源。

您对如何从属性文件中完全删除数据源的定义并从MultiTenantConnectionProviderImpl 以编程方式将数据源注入应用程序有什么建议吗?

【问题讨论】:

【参考方案1】:

以下是有关如何以编程方式创建数据源的完整示例。

只是参数取自属性文件,而不是在 java 类中进行硬编码。

当您定义多个数据源时,您必须定义一个@Primary,并且只定义一个,那么您将拥有一个标识每个数据源的@Qualifier。如何管理它们应该很简单。

@RequiredArgsConstructor
@PropertySource("classpath:persistence/persistence-primarydb.properties")
@EnableJpaRepositories(basePackages = "io.vforge.cauldron.repository.primary",
        entityManagerFactoryRef = "primaryEntityManagerFactory",
        transactionManagerRef = "primaryTransactionManager")
@EnableJpaAuditing
@Configuration
public class CauldronPrimaryDatasource 

    private final Environment env;

    @Primary
    @Bean
    public LocalContainerEntityManagerFactoryBean primaryEntityManagerFactory() 
        LocalContainerEntityManagerFactoryBean em
                = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(primaryDataSource());
        em.setPackagesToScan("io.vforge.cauldron.model.primary");

        HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        em.setJpaVendorAdapter(vendorAdapter);
        HashMap<String, Object> properties = new HashMap<>();
        properties.put("hibernate.hbm2ddl.auto", env.getProperty("primary.hibernate.hbm2ddl.auto"));
        properties.put("hibernate.dialect", env.getProperty("primary.hibernate.dialect"));
        properties.put("hibernate.show_sql", env.getProperty("primary.hibernate.show_sql"));
        em.setJpaPropertyMap(properties);

        return em;
    

    @Primary
    @Bean
    public HikariDataSource primaryDataSource() 
        final HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(env.getProperty("primary.datasource.url"));
        dataSource.setUsername(env.getProperty("primary.datasource.username"));
        dataSource.setPassword(env.getProperty("primary.datasource.password"));
        return dataSource;
    

    @Primary
    @Bean
    public PlatformTransactionManager primaryTransactionManager() 
        JpaTransactionManager transactionManager= new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(
                primaryEntityManagerFactory().getObject());
        return transactionManager;
    


【讨论】:

以上是关于在 Hibernate 多租户配置中禁用 Spring 数据源配置的主要内容,如果未能解决你的问题,请参考以下文章

hibernate 和eclipselink的区别

多租户:Spring 与 Hibernate

Quartz 2 + Hibernate 4 多租户不稳定行为

如何使用 Spring + Hibernate 对实体进行自定义验证以进行多租户设置

Spring Boot + Spring Data 多租户

具有多租户休眠的 Spring-Data JPA