我们正在为多个消费者开发SaaS解决方案。此解决方案基于Spring,Wicket和Hibernate。我们的 数据库包含来自多个客户的数据我们决定按如下方式对数据库建模:
要使用此设置,我们使用具有以下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
声明?
答案 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绑定到线程以进行整个请求处理,在我看来,它是多余的。