Hibernate 多租户:在会话中更改租户

Bas*_*ord 5 java session spring hibernate servlets

我们正在为多个消费者开发 SaaS 解决方案。该解决方案基于 Spring、Wicket 和 Hibernate。我们的数据库包含来自多个客户的数据。我们决定对数据库建模如下:

  • public
    所有客户之间的共享数据,例如用户帐户,因为我们不知道用户属于哪个客户
  • 客户_1
  • 客户_2
  • ...

为了使用此设置,我们使用具有以下 TenantIdentifierResolver 的多租户设置:

public class TenantProviderImpl implements CurrentTenantIdentifierResolver {
    private static final ThreadLocal<String> tenant = new ThreadLocal<>();

    public static void setTenant(String tenant){
        TenantProviderImpl.tenant.set(tenant);
    }

    @Override
    public String resolveCurrentTenantIdentifier() {
        return tenant.get();
    }

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

    /**
     * Initialize a tenant by storing the tenant identifier in both the HTTP session and the ThreadLocal
     *
     * @param   String  tenant  Tenant identifier to be stored
     */
    public static void initTenant(String tenant) {
        HttpServletRequest req = ((ServletWebRequest) RequestCycle.get().getRequest()).getContainerRequest();
        req.getSession().setAttribute("tenant", tenant);
        TenantProviderImpl.setTenant(tenant);
    }
}
Run Code Online (Sandbox Code Playgroud)

initTenant方法由 servlet 过滤器为每个请求调用。在打开与数据库的连接之前处理此过滤器。

我们还实现了AbstractDataSourceBasedMultiTenantConnectionProviderImpl设置为我们的 hibernate.multi_tenant_connection_provider. 它SET search_path在每个请求之前发出一个查询。这对于通过上述 servlet 过滤器的请求来说就像魅力一样。

现在来解决我们真正的问题:我们的应用程序中有一些入口点没有通过 servlet 过滤器,例如一些 SOAP 端点。还有一些定时作业执行时没有通过 servlet 过滤器。这证明是一个问题。

作业/端点以某种方式接收一个值,该值可用于识别应与作业/端点请求关联的客户。这个唯一值通常映射在我们的public数据库模式中。因此,我们需要在知道关联哪个客户之前查询数据库。因此,Spring 会初始化一个完整的 Hibernate 会话。此会话具有我们的默认租户 ID,未映射到特定客户。但是,在我们将唯一值解析给客户之后,我们希望会话更改租户标识符。虽然这似乎不受支持,但没有 aHibernateSession.setTenantIdentifier(String)而有 SharedSessionContract.getTenantIdentifier().

我们认为我们有以下方法的解决方案:

org.hibernate.SessionFactory sessionFactory = getSessionFactory();
org.hibernate.Session session = null;
try
{
    session = getSession();
    if (session != null)
    {
       if(session.isDirty())
       {
          session.flush();
       }
       if(!session.getTransaction().wasCommitted())
       {
          session.getTransaction().commit();
       }

       session.disconnect();
       session.close();
       TransactionSynchronizationManager.unbindResource(sessionFactory);
    }
}
catch (HibernateException e)
{
    // NO-OP, apparently there was no session yet
}
TenantProviderImpl.setTenant(tenant);
session = sessionFactory.openSession();
TransactionSynchronizationManager.bindResource(sessionFactory, new SessionHolder(session));
return session;
Run Code Online (Sandbox Code Playgroud)

然而,这种方法似乎在作业/端点的上下文中不起作用,并导致HibernateException诸如“会话已关闭!” 或“交易未成功启动”。

我们有点迷茫,因为我们一直在努力寻找解决方案。是不是我们误解了什么?我们误解了什么?我们如何解决上述问题?

回顾:HibernateSession-s 不是由用户请求创建的,而是由定时作业或此类创建的,不会通过我们的 servlet 过滤器,因此在 Hibernate 会话开始之前没有关联的租户标识符。它们具有唯一值,我们可以通过查询数据库将其转换为租户标识符。我们如何告诉现有的 Hibernate 会话改变它的租户标识符,从而发出一个新的SET search_path语句?

Bas*_*ord 3

我们从未找到解决此问题的真正解决方案,但是链接到 Jira 票证的chimmi是其他人请求的这样的功能:https ://hibernate.atlassian.net/browse/HHH-9766

根据此票证,我们想要的行为目前不受支持。不过,我们找到了一种解决方法,因为我们实际想要使用此功能的次数是有限的,因此我们可以使用默认的 java 并发实现在单独的线程中运行这些操作。

通过在单独的线程中运行该操作,将创建一个新会话(因为该会话是线程绑定的)。将租户设置为跨线程共享的变量对我们来说非常重要。为此,我们在 CurrentTenantIdentifierResolver 中有一个静态变量。

为了在单独的线程中运行操作,我们实现了一个Callable. 这些可调用对象被实现为具有作用域的 Spring-bean prototype,因此每次请求时都会创建一个新实例(自动装配)。我们已经实现了自己的抽象实现,Callable它完成了接口call()定义的方法Callable,并且该实现启动了一个新的 HibernateSession。代码看起来有点像这样:

public abstract class OurCallable<TYPE> implements Callable<TYPE> {
    private final String tenantId;

    @Autowired
    private SessionFactory sessionFactory;

    // More fields here

    public OurCallable(String tenantId) {
        this.tenantId = tenantId;
    }

    @Override
    public final TYPE call() throws Exception {
        TenantProvider.setTenant(tenantId);
        startSession();

        try {
            return callInternal();
        } finally {
            stopSession();
        }
    }

    protected abstract TYPE callInternal();

    private void startSession(){
        // Implementation skipped for clarity
    }

    private void stopSession(){
        // Implementation skipped for clarity
    }
}
Run Code Online (Sandbox Code Playgroud)