使用Spring或Hibernate实现多数据库Grails应用的多租户功能

4

Grails有一个适用于单个数据库的多租户插件和一个适用于多个数据库的多租户插件,但是针对多个数据库的多租户插件已不再支持/维护。是否有一种方法可以使用Spring或Hibernate本身来实现多租户多数据库的Grails应用程序?


答案很可能是肯定的。这取决于你的需求。多租户并非对每个人都有完全相同的定义。你能提供的关于你需求的详细信息越多,你就能得到更好的答案。例如,你需要如何在请求中识别你的租户?它是否在 URL 中,基于主机?域名?源 IP?在登录时由用户选择?与登录相关联?是否有一个中央数据库用于安全性,并为每个租户指定一个单独的数据库等等。 - Joshua Moore
"安全性和每个租户独立的中央数据库?": 是的,Stormpath DB用于安全,而我们自己的DB(可能是独立的)用于租户信息。 "如何在请求中识别您的租户?": 我希望可以根据用户名(即“与登录相关”)进行识别。组织/租赁将在注册时选择。如果不可能,用户可以在特定于其租赁的子域上登录。 - Daniel
另一个问题是这样做有多难?需要尽快推出,所以我考虑使用Django。 - Daniel
3个回答

9
您可以使用Hibernate multitenancy,具体描述在这里:http://docs.jboss.org/hibernate/orm/4.3/devguide/en-US/html/ch16.html 或者你也可以考虑使用Spring的AbstractRoutingDataSource
总的想法是一个路由数据源充当了一个中间人 - 而“真正”的数据源可以基于查找键动态确定运行时。
您可以在这里找到更多信息:https://spring.io/blog/2007/01/23/dynamic-datasource-routing/。您可以在这里找到一个较新的文章,提供了一个示例,演示如何使用Hibernate。解决方案的要点可以在以下两个代码片段中找到:
public class MyRoutingDataSource extends AbstractRoutingDataSource{
    @Override
    protected Object determineCurrentLookupKey() {
        String language = LocaleContextHolder.getLocale().getLanguage();
        System.out.println("Language obtained: "+ language);
        return language;
    }
}

返回值将用作数据源的判别器,以下配置设置了映射

<bean id="dataSource" class="com.howtodoinjava.controller.MyRoutingDataSource">
   <property name="targetDataSources">
      <map key-type="java.lang.String">
         <entry key="en" value-ref="concreteDataSourceOne"/>
         <entry key="es" value-ref="concreteDataSourceTwo"/>
      </map>
   </property>
</bean>

有趣。我不知道Spring DataSource Routing。我要去看看这个。甚至可能尝试一下这个例子。如果在应用程序运行时还有一种添加/删除数据源的方法,那就太棒了(至少对于我所需的)。谢谢。无论如何,我认为没有一种方法可以做到这一点并保持GORM的所有好处。 - ionutab

2

在我们的情况下,我们使用LocalContainerEntityManagerFactoryBean,在其中创建一个multiTenantMySQLProvider。

 <bean class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean" id="entityManagerFactory">
    <property name="dataSource" ref="dataSource"/>
    <property name="jpaVendorAdapter">
        <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" />
    </property>
    <property name="packagesToScan" value="domain"/>
    <property name="jpaPropertyMap">
        <map>
            <entry key="hibernate.dialect" value="org.hibernate.dialect.MySQL5Dialect" />
            <entry key="javax.persistence.jdbc.driver" value="org.mariadb.jdbc.Driver" />
            <entry key="hibernate.show_sql" value="false" />
            <entry key="hibernate.multiTenancy" value="SCHEMA" />
            <entry key="hibernate.multi_tenant_connection_provider" value-ref="mySQLMultiTenantConnectionProvider" />
            <entry key="hibernate.tenant_identifier_resolver" value-ref="tenantIdentifierResolver" />
        </map>
    </property>
</bean>




   <bean id="tenantService"
      class="multitenancy.service.impl.TenantServiceImpl">
    <property name="defaultTenantId" value="${multitenancy.defaultTenantId}" />
    <property name="ldapTemplate" ref="ldapTemplate" />
</bean>

<bean id="connectionProvider"
      class="multitenancy.hibernate.ConnectionProviderImpl"  lazy-init="false">
    <property name="dataSource" ref="dataSource" />
</bean>

<bean id="mySQLMultiTenantConnectionProvider"
      class="multitenancy.hibernate.MySQLMultiTenantConnectionProviderImpl" lazy-init="false">
    <property name="connectionProvider" ref="connectionProvider" />
    <property name="tenantIdentifierForAny" value="${multitenancy.tenantIdentifierForAny}" />
    <property name="schemaPrefix" value="${multitenancy.schemaPrefix}" />
</bean>

<bean id="tenantIdentifierResolver"
      class="multitenancy.hibernate.TenantIdentifierResolverImpl" lazy-init="false">
    <property name="tenantService" ref="tenantService" />
</bean>

<bean id="tenantIdentifierSchedulerResolver"
      class="security.impl.TenantIdentifierSchedulerResolverImpl" lazy-init="false">
    <property name="ldapTemplate" ref="ldapTemplate" />
</bean>

这里是MySQLMultiTenantConnectionProviderImpl的实现

public class MySQLMultiTenantConnectionProviderImpl implements MultiTenantConnectionProvider, ServiceRegistryAwareService, Stoppable {


private static final Logger LOGGER = LoggerFactory.getLogger(MySQLMultiTenantConnectionProviderImpl.class);

@Setter
private ConnectionProvider connectionProvider;

@Setter
private String tenantIdentifierForAny;

@Setter
private String schemaPrefix;

@Override
public Connection getAnyConnection() throws SQLException {
    return connectionProvider.getConnection();
}

@Override
public void releaseAnyConnection(Connection connection) throws SQLException {
    connectionProvider.closeConnection( connection );
}

@Override
public Connection getConnection(String tenantIdentifier) throws SQLException {
    final Connection connection = getAnyConnection();
    String schema = schemaPrefix + tenantIdentifier;
    try {
        LOGGER.debug("setting schema in DB Connection : {}"  , schema);
        connection.createStatement().execute( "USE " + schema );
    }
    catch ( SQLException e ) {
        throw new HibernateException(
           "Could not alter JDBC connection to specified schema [" + schema + "]", e
        );
    }
    return connection;
}

@Override
public void releaseConnection(String tenantIdentifier, Connection connection) throws SQLException {
    try {
        connection.createStatement().execute( "USE " + tenantIdentifierForAny );
    }
    catch ( SQLException e ) {
        LOGGER.error(" error on releaseConnection. The connection will be not closed. SQLException : {}" , e);
        // on error, throw an exception to make sure the connection is not returned to the pool.
        throw new HibernateException(
            "Could not alter JDBC connection to specified schema [" + tenantIdentifier + "]", e
        );
    }
    // I follow the hibernate recommendation and we don't return the connetion to the pool.
    connectionProvider.closeConnection( connection );
}

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

@Override
public void stop() {

}

@Override
public boolean isUnwrappableAs(Class unwrapType) {
    return ConnectionProvider.class.equals( unwrapType ) ||
            MultiTenantConnectionProvider.class.equals( unwrapType ) ||
            AbstractMultiTenantConnectionProvider.class.isAssignableFrom( unwrapType );
}

@Override
public <T> T unwrap(Class<T> unwrapType) {
    if ( isUnwrappableAs( unwrapType ) ) {
        return (T) this;
    }
    throw new UnknownUnwrapTypeException( unwrapType );

}

@Override
public void injectServices(ServiceRegistryImplementor serviceRegistry) {


}

}


1
这是我使用基于SCHEMA的Hibernate多租户的方法。也许对你有所帮助。

applicationContext.xml

    ...
    <bean id="multiTenantConnectionProvider" class="org.myapp.MyAppMultiTenantConnectionProvider"/>

    <bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
        <property name="packagesToScan" value="org.myapp.entities"/>
        <property name="multiTenantConnectionProvider" ref="multiTenantConnectionProvider"/>
        <property name="hibernateProperties">
            <props>
                <prop key="hibernate.multiTenancy">SCHEMA</prop>
                <prop key="hibernate.tenant_identifier_resolver">org.myapp.MyAppTenantIdentifierResolver</prop>
                ...
            </props>
        </property>
    </bean>
    ...

MyAppMultiTenantConnectionProvider.java

public class MyAppMultiTenantConnectionProvider implements MultiTenantConnectionProvider {

    @Override
    public Connection getConnection(String tenantIdentifier) throws SQLException {
        //This is where your tenant resolving logic will be implemented
        return MyMultitenantConnectionPool.getConnection(tenantIdentifier);
    }
}

MyAppTenantIdentifierResolver.java

public class MyAppTenantIdentifierResolver implements CurrentTenantIdentifierResolver {

    @Override
    public String resolveCurrentTenantIdentifier() {
        /*
        This is where you determine which tenant to use.
        In this app SpringSecurity used for this purpose.
        TenantUser class extends org.springframework.security.core.userdetails.User with tenant information.   
        */ 
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null || !authentication.isAuthenticated()) return "";

        if (authentication.getPrincipal() instanceof TenantUser) {
            TenantUser user = (TenantUser) authentication.getPrincipal();
            return user.getTenant();
        } else return "";
    }

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

首先,我想感谢你。其次,我必须说我对你在这里提出的概念没有很好的掌握,并且我有点困惑。1. "MyMultitenantConnectionPool"类如何工作?它内部有特殊的逻辑还是只是根据租户实例化连接?2. "getConnection"是在会话实例化时调用还是每个请求调用一次?3. 是否有公共的工作演示? - ionutab
1&2)看起来每个请求都会调用getConnection(),因此“MyMultiTenantConnectionPool”必须实现连接实例化和适当的池化策略。我们的应用程序使用单独的数据库存储有关租户、租户状态、相应数据库URL等信息。此时,我有MyTenantManager来维护这个“元”数据库。因此,MyMultitenantConnectionPool从MyTenantManager检索db URL,并使用相应的池化策略实例化连接。3)对不起,但这是一个商业项目,我没有权利发布源代码。 - Lea32
谢谢。不确定您所说的“模式方法”是什么意思?我认为您指的是为租户共享一个数据库,每个租户有一个单独的模式,但是您的评论似乎表明了另外一种情况。 - Daniel
这个:分离的模式 - Lea32
我认为那并没有解决我的问题。 - Daniel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接