Hibernate multitenancy:在会话

时间:2015-06-10 13:10:12

标签: java spring hibernate session servlets

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

  • 公共
    所有客户之间的共享数据,例如用户帐户,因为我们不知道用户属于哪个客户
  • customer_1
  • customer_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);
    }
}

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

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

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

作业/端点以某种方式接收一个值,该值可用于识别应与哪个客户相关联 工作/端点请求。此唯一值通常映射到我们的public数据库架构中。因此,我们需要查询 我们知道哪个客户关联之前的数据库。因此Spring初始化一个完整的Hibernate会话。这个 会话具有我们的默认租户ID,并且未映射到特定客户。但是,在我们解决了这个独特之后 我们希望会话更改租户标识符的客户的价值。这似乎不支持 不是HibernateSession.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;

然而,此方法似乎不适用于作业/端点的上下文,并导致HibernateException如{ “会议结束了!”或“交易没有成功开始”。

我们有点迷失,因为我们一直试图寻找解决方案已有一段时间了。有什么我们误解了吗? 我们误解了什么?我们如何解决上述问题?

回顾:HibernateSession - 不是由用户请求创建的,而是由定时作业创建的,或者不是通过我们的servlet 在Hibernate会话启动之前,过滤器没有关联的租户标识符。他们有独特的价值观 我们可以通过查询数据库将其转换为租户标识符。我们怎样才能告诉现有的Hibernate 会话改变它的租户标识符,从而发出新的SET search_path声明?

3 个答案:

答案 0 :(得分:3)

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

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

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

为了在单独的线程中运行操作,我们实现了Callable。这些callable实现为范围为prototype的Spring-bean,因此每次请求(自动装配)时都会创建一个新实例。我们已经实现了我们自己的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
    }
}

答案 1 :(得分:0)

另一种解决方法是将代表2个不同租户进行数据库调用的请求分解为2个单独的请求。 首先,客户端在系统中询问其关联的租户,然后以给定租户作为参数创建新请求。 IMO,直到(以及如果)支持该功能,它是一个相对干净的选择。

答案 2 :(得分:0)

我发现另一个解决方法,感谢关于OpenSessionInViewFilter / OpenEntityManagerInViewInterceptor的@ bas-dalenoord注释,这使我朝这个方向发展,就是禁用这个拦截器。

这可以通过在 application.properties 或环境中设置 spring.jpa.open-in-view = false 轻松实现-variable。

OpenEntityManagerInViewInterceptor将JPA EntityManager绑定到线程以进行整个请求处理,在我看来,它是多余的。