#java #spring-boot #hibernate #jpa #multi-tenant
#java #весенняя загрузка #гибернация #jpa #многопользовательский
Вопрос:
Я пытаюсь заполнить мою систему с несколькими арендаторами (одна база данных, несколько схем) данными, но столкнулся с проблемой, которой не было, когда я использовал один и тот же код с одной базой данных. Я полностью ожидаю, что во время моего исследования я пропустил что-то очевидное.
Каждая схема будет содержать точно такую же структуру таблицы.
Вот мой контекст арендатора
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;
}
}
}
И мой HibernateConfig
@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;
}
}
И мой TenantConenctionProvider
@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
Как я уже говорил ранее, это работало нормально, когда это была единая база данных и схема. Я не уверен на 100%, где я ошибся. Должен ли я каким-то образом регистрировать схемы? Если да, то как я могу подключить новых арендаторов без повторного развертывания? Должен ли я использовать пользовательский запрос на этом этапе, который использует схему в репозитории?
Заранее благодарю вас за любую помощь или совет.
РЕДАКТИРОВАТЬ 1 Итак, теперь я преодолел свое первоначальное препятствие, проверив свойства гибернации, изменив конфигурацию гибернации следующим образом
@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.
Ответ №1:
Вы внедрили AsyncHandlerInterceptor
— перехватчик Spring. Также должен быть зарегистрирован 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;
}
}
Это важно, потому что здесь вы заполняете TenantContext с помощью tenant .
Вы отладили метод getConnection(String tenantIdentifier)
, значение которого равно tenantIdentifier?