使用Hibernate和Spring Boot的单数据库多模式的多租户将数据保存到错误的模式



我试图用数据为我的多租户(单个数据库,多个模式(系统播种,但遇到了一个问题,当我对单个数据库使用相同的代码时,这个问题并不存在。在我的研究过程中,我完全预料到我遗漏了一些显而易见的东西。

每个模式都将包含完全相同的表结构。

这是我的租户上下文

public class TenantContext {
public static final String DEFAULT_TENANT_IDENTIFIER = "public";
private static final ThreadLocal<String> TENANT_IDENTIFIER = new ThreadLocal<>();
public static void setTenant(String tenantIdentifier) {
TENANT_IDENTIFIER.set(tenantIdentifier);
}
public static void reset(String tenantIdentifier) {
TENANT_IDENTIFIER.remove();
}
@Component
public static class TenantIdentifierResolver implements CurrentTenantIdentifierResolver {
@Override
public String resolveCurrentTenantIdentifier() {
String currentTenantId = TENANT_IDENTIFIER.get();
return currentTenantId != null ?
currentTenantId :
DEFAULT_TENANT_IDENTIFIER;
}
@Override
public boolean validateExistingCurrentSessions() {
return false;
}
}
}

和我的Hibernate配置

@Configuration
public class HibernateConfig {
@Autowired
private JpaProperties jpaProperties;
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
return new HibernateJpaVendorAdapter();
}
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource,
MultiTenantConnectionProvider multiTenantConnectionProvider, CurrentTenantIdentifierResolver currentTenantIdentifierResolver) {
Map<String, Object> jpaPropertiesMap = new HashMap<>();
jpaPropertiesMap.putAll(jpaProperties.getProperties());
jpaPropertiesMap.put(Environment.MULTI_TENANT, MultiTenancyStrategy.SCHEMA);
jpaPropertiesMap.put(Environment.MULTI_TENANT_CONNECTION_PROVIDER, multiTenantConnectionProvider);
jpaPropertiesMap.put(Environment.MULTI_TENANT_IDENTIFIER_RESOLVER, TenantContext.TenantIdentifierResolver.class);
LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
entityManagerFactoryBean.setDataSource(dataSource);
entityManagerFactoryBean.setPackagesToScan(UppStudentAppBeApplication.class.getPackage().getName());
entityManagerFactoryBean.setJpaVendorAdapter(jpaVendorAdapter());
entityManagerFactoryBean.setJpaPropertyMap(jpaPropertiesMap);
return entityManagerFactoryBean;
}
}

和我的租户服务提供商

@Component
public class TenantConnectionProvider implements MultiTenantConnectionProvider {
private static Logger logger = LoggerFactory.getLogger(TenantConnectionProvider.class);
@Autowired
private DataSource dataSource;
public TenantConnectionProvider(DataSource dataSource) {
this.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 tenantIdentifier) throws SQLException {
logger.info("Get connection for tenant  " + String.join(":", tenantIdentifier ));
final Connection connection = getAnyConnection();
try {
//connection.createStatement().execute( String.format("SET SCHEMA "%s";", tenantIdentifier));
connection.setSchema(tenantIdentifier);
} catch ( SQLException e ) {
throw new HibernateException(
"Could not alter JDBC connection to specified schema [" +
tenantIdentifier + "]",
e
);
}
return connection;
}
@Override
public void releaseConnection(String tenantIdentifier, Connection connection) throws SQLException {
try {
//connection.createStatement().execute( String.format("SET SCHEMA "%s";", TenantContext.DEFAULT_TENANT_IDENTIFIER) );
connection.setSchema(TenantContext.DEFAULT_TENANT_IDENTIFIER);
} catch ( SQLException e ) {
throw new HibernateException(
"Could not alter JDBC connection to specified schema [" +
tenantIdentifier + "]",
e
);
}
releaseAnyConnection(connection);
}
@Override
public boolean supportsAggressiveRelease() {
return false;
}
@Override
public boolean isUnwrappableAs(Class unwrapType) {
return false;
}
@Override
public <T> T unwrap(Class<T> unwrapType) {
return null;
}
}

我调用我的种子类,该类使用flyway迁移构建我的租户和模式。

然后,我尝试循环浏览切换TenantContext的已保存租户。当调试似乎有效时。然而,当我尝试用回购做任何事情时,我会出现以下错误。

o.h.engine.jdbc.spi.SqlExceptionHelper:错误:列campus0.createdat不存在
提示:也许您想引用列"campus0.created_at">
位置:32

正如我之前所说,当它是一个单一的数据库和模式时,它可以很好地工作。我不能百分之百确定我哪里出了问题。我应该如何注册模式吗?如果是,我如何在不重新部署的情况下接纳新租户?在这个阶段,我应该使用一个使用repo中模式的自定义查询吗?

提前感谢您的帮助或建议。

EDIT 1因此,我现在已经通过检查hibernate属性克服了最初的障碍,因此,通过按以下更改hibernate配置

@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource,
MultiTenantConnectionProvider multiTenantConnectionProvider,
HibernateProperties hibernateProperties) {
Map<String, Object> jpaPropertiesMap = hibernateProperties.determineHibernateProperties(jpaProperties.getProperties(), new HibernateSettings());
//jpaPropertiesMap.putAll(jpaProperties.getProperties());
jpaPropertiesMap.put(Environment.MULTI_TENANT, MultiTenancyStrategy.SCHEMA);
jpaPropertiesMap.put(Environment.MULTI_TENANT_CONNECTION_PROVIDER, multiTenantConnectionProvider);
jpaPropertiesMap.put(Environment.MULTI_TENANT_IDENTIFIER_RESOLVER, TenantContext.TenantIdentifierResolver.class);
LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
entityManagerFactoryBean.setDataSource(dataSource);
entityManagerFactoryBean.setPackagesToScan(UppStudentAppBeApplication.class.getPackage().getName());
entityManagerFactoryBean.setJpaVendorAdapter(jpaVendorAdapter());
entityManagerFactoryBean.setJpaPropertyMap(jpaPropertiesMap);
return entityManagerFactoryBean;
}

这已经消除了上述命名错误。然而,现在它保存到我的默认模式,而不是TenantIdentifierResolver中设置的模式。

是否实现了Spring的AsyncHandlerInterceptor拦截器。也应在WebMvcConfigurer中注册。

@Component
public class TenantRequestInterceptor implements AsyncHandlerInterceptor{
private SecurityDomain securityDomain;
public TenantRequestInterceptor(SecurityDomain securityDomain) {
this.securityDomain = securityDomain;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
return Optional.ofNullable(request)
.map(req -> securityDomain.getTenantIdFromJwt(req))
.map(tenant -> setTenantContext(tenant))
.orElse(false);
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
TenantContext.reset();
}

private boolean setTenantContext(String tenant) {
TenantContext.setCurrentTenant(tenant);
return true;
}
}

这一点很重要,因为这里用tenant填充TenantContext。你调试过方法getConnection(String tenantIdentifier)吗?tenantIdentifier的值是多少?

最新更新