簡體   English   中英

使用帶有 Hibernate 和 Spring 的單數據庫多模式的多租戶引導將數據保存到錯誤的模式

[英]Multi-tenancy using single database multiple schema with Hibernate and Spring Boot saving data to wrong schema

我正在嘗試使用數據為我的多租戶(單個數據庫、多個模式)系統播種,但遇到了一個問題,當我對單個數據庫使用相同的代碼時,該問題不存在。 我完全期望在我的研究過程中我錯過了一些明顯的東西。

每個模式都將包含完全相同的表結構。

這是我的租戶上下文

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;
    }
}

我將我的種子稱為 class,它使用 flyway 遷移構建了我的租戶和模式。

然后我嘗試遍歷保存的租戶切換 TenantContext。 當調試似乎起作用時。 但是,當我嘗試對 repo 做任何事情時,我收到以下錯誤。

ohengine.jdbc.spi.SqlExceptionHelper: 錯誤: 列 Campus0_.createdat 不存在
提示:也許您的意思是引用列“campus0_.created_at”。
Position:32

正如我之前所說,當它是單個數據庫和模式時,它以前工作得很好。 我不是 100% 確定我哪里出錯了。 我應該如何注冊模式? 如果是這樣,我如何在不重新部署的情況下加入新租戶? 我應該在這個階段使用使用 repo 中的模式的自定義查詢嗎?

提前感謝您的任何幫助或建議。

編輯 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 中設置的架構。

你實現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。 您是否調試過方法getConnection(String tenantIdentifier)作為tenantIdentifier 的值是什么?

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM