Spring Boot + Hibernate + Oracle 模式多租户
Spring Boot + Hibernate + Oracle schema multitenancy
我正在尝试让基于模式的多租户解决方案工作,类似于 this example 但使用 Oracle 而不是 Postgres。
例如,我有三个模式:FOO、BAR 和 BAZ。 BAR 和 BAZ 各有一个 table 称为 MESSAGES。 BAR.MESSAGES 和 BAZ.MESSAGES 均已授予 FOO SELECT。因此,如果我以 FOO 身份连接,然后执行
SELECT * FROM BAR.MESSAGES;
然后我得到了预期的结果。但是,如果我省略模式名称(例如 SELECT * FROM MESSAGES),则会得到 ORA-00942:table 或视图不存在(连接使用错误的模式)。
这是我的 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。文档说:
The user also has an option to append the CURRENT_USER value to the
ADT name to obtain fully qualified name by setting this property to
true. Note that it takes a network round trip to fetch the
CURRENT_SCHEMA value.
但即使我将其设置为 true (-Doracle.jdbc.createDescriptorUseCurrentSchemaForSchemaName=true),问题仍然存在。这可能是因为 Connection 上的 "username" 仍然是 "FOO",即使在更改会话以将架构设置为 "BAR" 之后(Connection 上的 currentSchema 是 "BAR")。但这意味着 OracleConnection 文档不正确,不是吗?
更新 2
我没有包括我们在这里也使用 Spring Data JPA 的事实。也许这与问题有关?
我发现,如果我在实体中包含硬编码的架构名称,那么它就可以工作(例如@Table(schema="BAR")),但是具有硬编码的值没有接受table 解决方案。
如果我们将查询重写为原生 @Query 然后在 SQL 中包含 {h-schema} 也可能有效,但在 Hibernate 中这是默认模式,而不是 'current' (动态)架构,所以它也不太正确。
原来在Controller第一行这样设置当前租户(TenantContext.setCurrentTenant("BAR"))是"too late"(Spring已经创建交易?)。我将实现更改为使用 servlet 过滤器将租户 ID 从 header 设置为请求属性,然后在 TenantIdentifierResolver 中获取该属性,而不是使用 TenantContext。现在它可以正常工作,没有我在更新中提到的任何内容。
我正在尝试让基于模式的多租户解决方案工作,类似于 this example 但使用 Oracle 而不是 Postgres。
例如,我有三个模式:FOO、BAR 和 BAZ。 BAR 和 BAZ 各有一个 table 称为 MESSAGES。 BAR.MESSAGES 和 BAZ.MESSAGES 均已授予 FOO SELECT。因此,如果我以 FOO 身份连接,然后执行
SELECT * FROM BAR.MESSAGES;
然后我得到了预期的结果。但是,如果我省略模式名称(例如 SELECT * FROM MESSAGES),则会得到 ORA-00942:table 或视图不存在(连接使用错误的模式)。
这是我的 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。文档说:
The user also has an option to append the CURRENT_USER value to the ADT name to obtain fully qualified name by setting this property to true. Note that it takes a network round trip to fetch the CURRENT_SCHEMA value.
但即使我将其设置为 true (-Doracle.jdbc.createDescriptorUseCurrentSchemaForSchemaName=true),问题仍然存在。这可能是因为 Connection 上的 "username" 仍然是 "FOO",即使在更改会话以将架构设置为 "BAR" 之后(Connection 上的 currentSchema 是 "BAR")。但这意味着 OracleConnection 文档不正确,不是吗?
更新 2 我没有包括我们在这里也使用 Spring Data JPA 的事实。也许这与问题有关? 我发现,如果我在实体中包含硬编码的架构名称,那么它就可以工作(例如@Table(schema="BAR")),但是具有硬编码的值没有接受table 解决方案。
如果我们将查询重写为原生 @Query 然后在 SQL 中包含 {h-schema} 也可能有效,但在 Hibernate 中这是默认模式,而不是 'current' (动态)架构,所以它也不太正确。
原来在Controller第一行这样设置当前租户(TenantContext.setCurrentTenant("BAR"))是"too late"(Spring已经创建交易?)。我将实现更改为使用 servlet 过滤器将租户 ID 从 header 设置为请求属性,然后在 TenantIdentifierResolver 中获取该属性,而不是使用 TenantContext。现在它可以正常工作,没有我在更新中提到的任何内容。