Многопользовательская работа с использованием одной базы данных, множественной схемы с гибернацией и сохранением данных при загрузке Spring в неправильную схему

#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?