【问题标题】:Hibernate multitenancy: change tenant in session休眠多租户:在会话中更改租户
【发布时间】:2015-08-25 18:21:02
【问题描述】:

我们正在为多个消费者开发 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);
    }
}

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

我们还实现了一个AbstractDataSourceBasedMultiTenantConnectionProviderImpl,它被设置为我们的 hibernate.multi_tenant_connection_provider。它在每个请求之前发出SET search_path 查询。这对通过上述 servlet 过滤器的请求很有效。

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

作业/端点以某种方式接收一个值,该值可用于识别应与哪个客户相关联 作业/端点请求。这个唯一值通常映射到我们的public 数据库模式中。因此,我们需要查询 在我们知道与哪个客户相关联之前的数据库。因此,Spring 初始化了一个完整的 Hibernate 会话。这 session 具有我们的默认租户 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;

但是,此方法似乎在 Job/Endpoint 的上下文中不起作用,并导致 HibernateException 例如 “会议结束!”或“交易未成功启动”。

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

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

【问题讨论】:

  • 有点晚了,但这似乎很相关hibernate.atlassian.net/browse/HHH-9766
  • 看来你是对的,ticket 指出这实际上是迄今为止不受支持的行为。我们找到了一种解决方法,我将在下面描述...
  • 我在这里很晚了,但是 AOP 会帮助你吗?我不确定,但在执行 SOAP 端点(在路径中包含tenantId)和石英调度程序(我认为有一种方法可以将数据传递给调度程序,我们可以在那里传递tenantId)之前可能会有所帮助?
  • 我不确定 AOP 是否有帮助,我对 AOP 的可能性不太熟悉。该问题与我们使用 OpenSessionInViewFilter 所做的设计选择有关,其目标是防止在单个请求线程中使用多个会话。这与 Hibernate 不允许在打开的会话中切换租户的事实相结合,这意味着它不容易实现。我们已经构建了另一个没有 OpenSessionInViewFilter 的基于 Hibernate 的应用程序,这根本不是问题。
  • @Ermintar 的这个解决方案对我来说效果很好。 stackoverflow.com/questions/46847059/…

标签: java spring hibernate session servlets


【解决方案1】:

感谢@bas-dalenoord 对 OpenSessionInViewFilter/OpenEntityManagerInViewInterceptor 的评论,我发现了另一个解决方法,它引导我走向这个方向,是禁用这个拦截器。

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

OpenEntityManagerInViewInterceptor 将 JPA EntityManager 绑定到线程以完成请求的整个处理,在我的情况下它是多余的。

【讨论】:

  • 这可以更好地记录下来......我浪费了几个小时来改变和玩东西,研究和打我的桌子,直到我找到你的anware......谢谢你,伙计!
【解决方案2】:

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

【讨论】:

    【解决方案3】:

    我们从来没有为这个问题找到真正的解决方案,但是链接到 Jira-ticket 的 chimmi 是其他人要求这样的功能:https://hibernate.atlassian.net/browse/HHH-9766

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

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

    为了在单独的线程中运行操作,我们实现了Callable。这些可调用对象作为 Spring-beans 实现,范围为prototype,因此每次请求时都会创建一个新实例(自动装配)。我们已经实现了我们自己的Callable 抽象实现,它最终确定了Callable 接口定义的call() 方法,并且实现启动了一个新的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
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-04-01
      • 1970-01-01
      • 1970-01-01
      • 2022-01-17
      • 1970-01-01
      • 2021-06-19
      • 2014-12-01
      • 2015-03-20
      相关资源
      最近更新 更多