Spring Boot + Hibernate + Oracle模式多租户

时间:2019-01-29 22:41:28

标签: spring oracle spring-boot multi-tenant hikaricp

我正在尝试使基于模式的多租户解决方案正常工作,类似于this example,但使用Oracle而不是Postgres。

例如,我有三种模式:FOO,BAR和BAZ。 BAR和BAZ都有一个称为MESSAGES的表。 FOO已在BAR.MESSAGES和BAZ.MESSAGES上被授予SELECT。因此,如果我以FOO身份连接,然后执行

SELECT * FROM BAR.MESSAGES;

然后我得到预期的结果。但是,如果我遗漏了架构名称(例如SELECT * FROM MESSAGES),则会得到ORA-00942:表或视图不存在(连接使用了错误的架构)。

这是我的Dao /存储库:

@Repository
public interface MessageDao extends CrudRepository<Foo, Long> {
}

控制器:

@GetMapping("/findAll")
public List<Message> findAll() {
    TenantContext.setCurrentTenant("BAR");
    var result = messageDao.findAll();
    return result;
}

配置:

@Configuration
public class MessageConfig {
    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        return new HibernateJpaVendorAdapter();
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource,
                                                                       MultiTenantConnectionProvider multiTenantConnectionProvider,
                                                                       CurrentTenantIdentifierResolver tenantIdentifierResolver) {
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(dataSource);
        em.setPackagesToScan(Message.class.getPackageName());

        em.setJpaVendorAdapter(this.jpaVendorAdapter());

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

        em.setJpaPropertyMap(jpaProperties);
        return em;
    }

MultitenantConnectionProvider:

@Component
public class MultiTenantConnectionProviderImpl implements MultiTenantConnectionProvider {

    @Autowired
    private DataSource dataSource;

    @Override
    public Connection getAnyConnection() throws SQLException {
        return dataSource.getConnection();
    }

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

    @Override
    public Connection getConnection(String currentTenantIdentifier) throws SQLException {
        String tenantIdentifier = TenantContext.getCurrentTenant();
        final Connection connection = getAnyConnection();
        try (Statement statement = connection.createStatement()) {
            statement.execute("ALTER SESSION SET CURRENT_SCHEMA = BAR");
        } catch (SQLException e) {
            throw new HibernateException("Problem setting schema to " + tenantIdentifier, e);
        }
        return connection;
    }

    @Override
    public void releaseConnection(String tenantIdentifier, Connection connection) throws SQLException {
        try (Statement statement = connection.createStatement()) {
            statement.execute("ALTER SESSION SET CURRENT_SCHEMA = FOO");
        } catch (SQLException e) {
            throw new HibernateException("Problem setting schema to " + tenantIdentifier, e);
        }
        connection.close();
    }

    @SuppressWarnings("rawtypes")
    @Override
    public boolean isUnwrappableAs(Class unwrapType) {
        return false;
    }

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

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

和TenantIdentifierResolver(尽管并不十分相关,因为我现在正在上面的ConnectionProviderImpl中对租户进行硬编码):

@Component
public class TenantIdentifierResolver implements CurrentTenantIdentifierResolver {

    @Override
    public String resolveCurrentTenantIdentifier() {
        String tenantId = TenantContext.getCurrentTenant();
        if (tenantId != null) {
            return tenantId;
        }
        return "BAR";
    }

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

关于底层Connection为什么未按预期切换架构的任何想法?

更新1

也许与底层的Oracle连接有关。有a property on OracleConnection named CONNECTION_PROPERTY_CREATE_DESCRIPTOR_USE_CURRENT_SCHEMA_FOR_SCHEMA_NAME。该文档说:

  

用户还可以选择将CURRENT_USER值附加到   通过将此属性设置为ADT名称以获得完全限定的名称   真正。请注意,网络往返需要提取   CURRENT_SCHEMA值。

但是即使将其设置为true(-Doracle.jdbc.createDescriptorUseCurrentSchemaForSchemaName = true),问题仍然存在。这可能是因为,即使在更改会话以将架构设置为“ BAR”(连接上的currentSchema为“ BAR”)之后,Connection上的“用户名”仍为“ FOO”。但这意味着OracleConnection文档不正确,不是吗?

更新2 我没有在这里包括我们也在使用Spring Data JPA的事实。也许与问题有关? 我发现,如果我在实体中包含硬编码的架构名称,那么它就可以工作(例如@Table(schema =“ BAR”)),但具有硬编码的值是无法接受的解决方案。

如果我们将查询重写为本机@Query,然后在SQL中包含{h-schema},它也可能会起作用,但是在Hibernate中,这是默认模式,而不是“当前”(动态)模式,因此不是还是很正确的。

1 个答案:

答案 0 :(得分:0)

事实证明,在Controller的第一行上像(TenantContext.setCurrentTenant(“ BAR”))这样设置当前租户“太迟了”(Spring已经创建了事务?)。我更改了实现,以使用Servlet筛选器将承租人ID从标头设置为请求属性,然后在TenantIdentifierResolver中获取该属性,而不是使用TenantContext。现在它可以正常工作了,没有我在更新中提到的任何内容。