You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Quarkus:运行时动态更新属性(多租户数据库切换)

Hibernate 独立数据库模式多租户切换实现方案

针对你开发的多租户应用需求(用户登录后切换到对应公司的独立数据库),可以基于Hibernate的DATABASE多租户策略来实现,以下是具体步骤:

1. 配置Hibernate多租户基础属性

替换application.properties中的单一数据库配置,添加多租户相关配置:

# Hibernate核心配置
spring.jpa.hibernate.ddl-auto=none
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect

# 多租户配置
spring.jpa.properties.hibernate.multiTenancy=DATABASE
# 指定租户标识符解析器
spring.jpa.properties.hibernate.tenant_identifier_resolver=com.yourproject.config.CustomTenantIdentifierResolver
# 指定多租户连接提供者
spring.jpa.properties.hibernate.multi_tenant_connection_provider=com.yourproject.config.CustomMultiTenantConnectionProvider

2. 实现租户标识符解析器

这个类的作用是获取当前登录用户对应的租户(公司)数据库标识,从安全上下文直接读取用户信息即可:

package com.yourproject.config;

import org.hibernate.context.spi.CurrentTenantIdentifierResolver;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;

public class CustomTenantIdentifierResolver implements CurrentTenantIdentifierResolver {

    @Override
    public String resolveCurrentTenantIdentifier() {
        // 从登录上下文获取当前用户的租户标识
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (auth != null && auth.getPrincipal() instanceof CustomUserDetails) {
            CustomUserDetails userDetails = (CustomUserDetails) auth.getPrincipal();
            // 返回用户对应的公司数据库标识(比如公司ID对应的DB名称)
            return userDetails.getCompanyDbId();
        }
        // 未登录时返回默认数据库标识
        return "default_db";
    }

    @Override
    public boolean validateExistingCurrentSessions() {
        return true;
    }
}

3. 实现多租户连接提供者

该类负责根据租户标识动态获取对应的数据库连接,建议用连接池管理租户数据源:

package com.yourproject.config;

import org.hibernate.engine.jdbc.connections.spi.MultiTenantConnectionProvider;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class CustomMultiTenantConnectionProvider implements MultiTenantConnectionProvider {

    private final Map<String, DataSource> tenantDataSources = new ConcurrentHashMap<>();
    private final DataSource defaultDataSource;

    // 注入默认数据源(用于初始化或默认租户)
    public CustomMultiTenantConnectionProvider(DataSource defaultDataSource) {
        this.defaultDataSource = defaultDataSource;
    }

    @Override
    public Connection getConnection(String tenantIdentifier) throws SQLException {
        // 优先从缓存取租户数据源
        if (tenantDataSources.containsKey(tenantIdentifier)) {
            return tenantDataSources.get(tenantIdentifier).getConnection();
        }
        // 按需创建租户数据源(实际可从配置文件/租户管理表读取DB信息)
        DataSource tenantDs = createTenantDataSource(tenantIdentifier);
        tenantDataSources.put(tenantIdentifier, tenantDs);
        return tenantDs.getConnection();
    }

    private DataSource createTenantDataSource(String tenantId) {
        HikariConfig config = new HikariConfig();
        // 从配置读取租户DB信息(示例:application.properties中配置tenant.db.{tenantId}.url等)
        config.setJdbcUrl(getProperty("tenant.db." + tenantId + ".url"));
        config.setUsername(getProperty("tenant.db." + tenantId + ".username"));
        config.setPassword(getProperty("tenant.db." + tenantId + ".password"));
        // 连接池配置
        config.setMaximumPoolSize(8);
        config.setConnectionTimeout(30000);
        return new HikariDataSource(config);
    }

    // 封装配置读取方法,实际可使用Spring的Environment
    private String getProperty(String key) {
        // 示例实现,实际替换为Spring Environment的getProperty方法
        return System.getProperty(key);
    }

    @Override
    public void releaseConnection(String tenantIdentifier, Connection connection) throws SQLException {
        connection.close();
    }

    @Override
    public boolean supportsAggressiveRelease() {
        return false;
    }

    @Override
    public <T> T unwrap(Class<T> unwrapType) {
        if (unwrapType.isInstance(this)) {
            return (T) this;
        }
        throw new IllegalArgumentException("无法转换为类型: " + unwrapType.getName());
    }

    @Override
    public boolean isUnwrappableAs(Class<?> unwrapType) {
        return MultiTenantConnectionProvider.class.isAssignableFrom(unwrapType);
    }
}

4. 登录时绑定租户信息

在用户登录成功后,将对应的公司数据库标识存入自定义的UserDetails中,确保后续请求能获取到租户标识:

// 自定义UserDetails类,扩展租户标识字段
public class CustomUserDetails implements UserDetails {

    private final String username;
    private final String password;
    private final String companyDbId;
    private final Collection<? extends GrantedAuthority> authorities;

    public CustomUserDetails(String username, String password, String companyDbId) {
        this.username = username;
        this.password = password;
        this.companyDbId = companyDbId;
        this.authorities = List.of(new SimpleGrantedAuthority("ROLE_USER"));
    }

    public String getCompanyDbId() {
        return companyDbId;
    }

    // 实现UserDetails其他必要方法...
    @Override
    public String getUsername() { return username; }
    @Override
    public String getPassword() { return password; }
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; }
    // 省略isAccountNonExpired、isAccountNonLocked等方法实现
}

关键注意事项

  • 所有租户的数据库结构必须完全一致,否则Hibernate实体映射会出现异常
  • 连接池配置要根据租户数量调整,避免连接数耗尽
  • 若支持动态新增租户,需实现数据源的动态刷新逻辑(比如监听租户新增事件,更新tenantDataSources缓存)
  • 可添加租户数据源的过期清理机制,释放闲置资源

内容的提问来源于stack exchange,提问作者Milton Marino Lopez Montero

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 04:27:15