Spring + Hibernate下的多租户: "配置了多租户SessionFactory,但未指定租户标识符"

20
在一个Spring 3应用程序中,我正在尝试通过Hibernate 4的本地MultiTenantConnectionProviderCurrentTenantIdentifierResolver实现多租户。我注意到在Hibernate 4.1.3中存在问题, 但我正在运行4.1.9并仍然遇到类似的异常:
   Caused by:

org.hibernate.HibernateException: SessionFactory configured for multi-tenancy, but no tenant identifier specified
    at org.hibernate.internal.AbstractSessionImpl.<init>(AbstractSessionImpl.java:84)
    at org.hibernate.internal.SessionImpl.<init>(SessionImpl.java:239)
    at org.hibernate.internal.SessionFactoryImpl$SessionBuilderImpl.openSession(SessionFactoryImpl.java:1597)
    at org.hibernate.internal.SessionFactoryImpl.openSession(SessionFactoryImpl.java:963)
    at org.springframework.orm.hibernate4.HibernateTransactionManager.doBegin(HibernateTransactionManager.java:328)
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction(AbstractPlatformTransactionManager.java:371)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.createTransactionIfNecessary(TransactionAspectSupport.java:334)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:105)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:631)
    at com.afflatus.edu.thoth.repository.UserRepository$$EnhancerByCGLIB$$c844ce96.getAllUsers(<generated>)
    at com.afflatus.edu.thoth.service.UserService.getAllUsers(UserService.java:29)
    at com.afflatus.edu.thoth.HomeController.hello(HomeController.java:37)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:601)
    at org.springframework.web.method.support.InvocableHandlerMethod.invoke(InvocableHandlerMethod.java:219)
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:132)
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:104)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandleMethod(RequestMappingHandlerAdapter.java:746)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:687)
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:80)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:925)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:856)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:915)
    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:811)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:735)
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:796)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:848)
    at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:671)
    at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:448)
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:138)
    at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:564)
    at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:213)
    at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1070)
    at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:375)
    at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:175)
    at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1004)
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:136)
    at org.eclipse.jetty.server.handler.ContextHandlerCollection.handle(ContextHandlerCollection.java:258)
    at org.eclipse.jetty.server.handler.HandlerCollection.handle(HandlerCollection.java:109)
    at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:97)
    at org.eclipse.jetty.server.Server.handle(Server.java:439)
    at org.eclipse.jetty.server.HttpChannel.run(HttpChannel.java:246)
    at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:265)
    at org.eclipse.jetty.io.AbstractConnection$ReadCallback.run(AbstractConnection.java:240)
    at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:589)
    at org.eclipse.jetty.util.thread.QueuedThreadPool$3.run(QueuedThreadPool.java:520)
    at java.lang.Thread.run(Thread.java:722) enter code here

下面是相关代码。在MultiTenantConnectionProvider中,我目前只写了一些简单的愚蠢代码,每次都返回一个新的连接,并且CurrentTenantIdentifierResolver当前总是返回相同的ID。显然,在我成功地实例化连接之后,需要实现这种逻辑。

config.xml

<bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
    <property name="dataSource" ref="dataSource" />
    <property name="packagesToScan">
        <list>
            <value>com.afflatus.edu.thoth.entity</value>
        </list>
    </property>
    <property name="hibernateProperties">
        <props>
            <prop key="hibernate.dialect">${hibernate.dialect}</prop>
            <prop key="hibernate.show_sql">${hibernate.show_sql}</prop>
            <prop key="hibernate.hbm2ddl">${hibernate.dbm2ddl}</prop>
            <prop key="hibernate.multiTenancy">DATABASE</prop>
            <prop key="hibernate.multi_tenant_connection_provider">com.afflatus.edu.thoth.connection.MultiTenantConnectionProviderImpl</prop>
            <prop key="hibernate.tenant_identifier_resolver">com.afflatus.edu.thoth.context.MultiTenantIdentifierResolverImpl</prop>
        </props>
    </property>
</bean>

<bean id="transactionManager" class="org.springframework.orm.hibernate4.HibernateTransactionManager">
    <property name="autodetectDataSource" value="false" />
    <property name="sessionFactory" ref="sessionFactory" />
</bean>

MultiTenantConnectionProvider.java

package com.afflatus.edu.thoth.connection;

import java.util.Properties;
import java.util.HashMap;
import java.util.Map;

import org.hibernate.service.jdbc.connections.spi.AbstractMultiTenantConnectionProvider;
import org.hibernate.service.jdbc.connections.spi.ConnectionProvider;
import org.hibernate.ejb.connection.InjectedDataSourceConnectionProvider;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.hibernate.cfg.*;

public class MultiTenantConnectionProviderImpl extends AbstractMultiTenantConnectionProvider {

    private final Map<String, ConnectionProvider> connectionProviders
        = new HashMap<String, ConnectionProvider>();

    @Override
    protected ConnectionProvider getAnyConnectionProvider() {

        System.out.println("barfoo");
        Properties properties = getConnectionProperties();

        DriverManagerDataSource ds = new DriverManagerDataSource();
        ds.setDriverClassName("com.mysql.jdbc.Driver");
        ds.setUrl("jdbc:mysql://127.0.0.1:3306/test");
        ds.setUsername("root");
        ds.setPassword("");

        InjectedDataSourceConnectionProvider defaultProvider = new InjectedDataSourceConnectionProvider();
        defaultProvider.setDataSource(ds);
        defaultProvider.configure(properties);

        return (ConnectionProvider) defaultProvider;
    }


    @Override
    protected ConnectionProvider selectConnectionProvider(String tenantIdentifier) {
        System.out.println("foobar");
        Properties properties = getConnectionProperties();

        DriverManagerDataSource ds = new DriverManagerDataSource();
        ds.setDriverClassName("com.mysql.jdbc.Driver");
        ds.setUrl("jdbc:mysql://127.0.0.1:3306/test2");
        ds.setUsername("root");
        ds.setPassword("");

        InjectedDataSourceConnectionProvider defaultProvider = new InjectedDataSourceConnectionProvider();
        defaultProvider.setDataSource(ds);
        defaultProvider.configure(properties);

        return (ConnectionProvider) defaultProvider;
    }

    private Properties getConnectionProperties() {
        Properties properties = new Properties();
        properties.put(AvailableSettings.DIALECT, "org.hibernate.dialect.MySQLDialect");
        properties.put(AvailableSettings.DRIVER, "com.mysql.jdbc.Driver");
        properties.put(AvailableSettings.URL, "jdbc:mysql://127.0.0.1:3306/test");
        properties.put(AvailableSettings.USER, "root");
        properties.put(AvailableSettings.PASS, "");

        return properties;

    }
}

CurrentTenantIdentifierResolver.java

package com.afflatus.edu.thoth.context;

import org.hibernate.context.spi.CurrentTenantIdentifierResolver;

public class CurrentTenantIdentifierResolverImpl implements CurrentTenantIdentifierResolver {

    public String resolveCurrentTenantIdentifier() {
        return "1";
    }

    public boolean validateExistingCurrentSessions() {
        return true;
    }

}

有人能看出具体的错误吗?一旦开启事务就会抛出异常。看起来SessionFactory没有正确打开Session,或者Session简单地忽略了由CurrentTenantIdentifierResolver返回的值,我认为这是Hibernate 4.1.3中的问题;这应该已经解决了。

7个回答

18
您是否在代码中任何地方使用了@Transactional(例如标记服务或DAO类/方法)?我遇到了相同的错误,直到我注释了服务类中的@Transactional。我认为这与Hibernate 4默认的openSessionInThread行为有关。
我还没有使用自定义实现的ConnectionProviderTenantIdentifierResolver来配置hibernate。我正在使用基于jndi的方法,将hibernate.connection.datasource设置为java://comp/env/jdbc/,然后将jndi资源的名称传递给调用我的dao方法的方法。 sessionFactory.withOptions().tenantIdentifier(tenant).openSession(); 我仍在尝试使用@Transactional进行配置,但使用基于jndi的方法和默认的线程中的会话行为似乎已经可以工作了。

我正在使用 @Transactional,并且我认为你的理论是正确的。你能提供一些关于你的 Jndi 解决方案的代码示例吗?我打算今天试试看,看看能否让它正常工作。 - Craige
我根据你在这里的反馈制定了一个解决方案。虽然我还不是100%确定,但我正在使用AOP来包装我的服务层方法,并进行手动事务处理。这将保持我的服务层和DAO类的公共接口干净。我仍然使用ConnectionProvider和TenantIdentifierResolver,因此当这个问题在未来得到修补(我认为这是一个错误)时,所有我的接口都保持不变,我只需删除包装我的服务层的方面即可。我很快会发布相关代码。 - Craige
@Craige,你成功让它工作了吗?你能分享一下代码吗? - Diego Plentz

11

前言:虽然我接受了这个包含代码的答案,请如果您认为这个回答有用,请点赞Darren的答案。他是我能够解决这个问题的原因。

好的,我们开始吧...

正如Darren所指出的那样, 这实际上是一个SessionFactory错误地实例化Session的问题。如果您手动实例化会话,就不会出现问题。例如:

sessionFactory.withOptions().tenantIdentifier(tenant).openSession();

然而,@Transactional 注解会导致 SessionFactory 使用 sessionFactory.getCurrentSession() 打开一个会话,该会话不会从 CurrentTenantIdentifierResolver 中获取租户标识符。
Darren 建议在 DAO 层手动打开会话,但这意味着每个 DAO 方法都将有一个局部范围的事务。更好的做法是在服务层上执行此操作。每个服务层调用(例如,doSomeLogicalTask())可能调用多个 DAO 方法。它有意义将每个方法绑定到同一事务,因为它们在逻辑上相关。
此外,我不喜欢在每个服务层方法中重复创建和管理事务的想法。相反,我使用 AOP 来包装我的服务层中的每个方法,以便实例化一个新的 Session 并处理事务。该方面将当前的 Session 存储在可以由 DAO 层访问的 TheadLocal 栈中以进行查询。
这项工作将使界面和实现与其修复后的版本完全相同,除了DAO超类中的一行代码将从ThreadLocal堆栈中获取Session而不是SessionFactory。一旦修复了该错误,就可以更改此设置。如果有任何问题,请随时在下面讨论。我很快会发布代码,稍微整理一下。请保留HTML标记。

请问这个问题在当前最新的Hibernate版本中是否已经解决?如果可能的话,请分享您的工作代码示例。 - VKPRO
已经测试过使用Hibernate 4.2.6,现在它可以正常工作了。这个问题在这个版本中得到了解决。 - VKPRO
请问您能提供一个工作示例吗?我也在使用@Transaction相同的模式。 - Dyapa Srikanth

3
Hibernate定义了CurrentTenantIdentifierResolver接口,以帮助像Spring或Java EE这样的框架允许使用默认的Session实例化机制(无论是来自EntityManagerFactory)。因此,必须通过配置属性设置CurrentTenantIdentifierResolver,而这正是您出错的地方,因为您没有提供正确的完全限定类名。由于CurrentTenantIdentifierResolver实现是CurrentTenantIdentifierResolverImpl,因此hibernate.tenant_identifier_resolver必须是:
<prop key="hibernate.tenant_identifier_resolver">com.afflatus.edu.thoth.context.CurrentTenantIdentifierResolverImpl</prop>

在你修复这个问题后,当 HibernateTransactionManager 调用 getSessionFactory().openSession() 时,Hibernate 将使用 CurrentTenantIdentifierResolverImpl 来解析租户标识符。

2
尽管这可能是一个较旧的主题,答案可能已经得到解决。但我注意到以下内容:
在定义类CurrentTenantIdentifierResolverImpl时:
public class CurrentTenantIdentifierResolverImpl implements CurrentTenantIdentifierResolver

但是在你的配置中,你引用了MultiTenantIdentifierResolverImpl:
<prop key="hibernate.tenant_identifier_resolver">com.afflatus.edu.thoth.context.MultiTenantIdentifierResolverImpl</prop>

我想指出这一点,因为我今天也犯了同样的错误,之后一切都非常顺利。


1

当我的CurrentTenantIdentifierResolver实现返回resolveCurrentTenantIdentifier()方法的null时,我遇到了类似的问题。


1

我使用了Spring Boot 3.0.1和Hibernate 6.1.6.Final,并通过实现接口HibernatePropertiesCustomizer来解决了这个问题。

@Configuration
public class CurrentTenantIdentifierResolverImpl implements CurrentTenantIdentifierResolver, HibernatePropertiesCustomizer {

    private String tenantId = "default";

    @Override
    public String resolveCurrentTenantIdentifier() {
        return tenantId;
    }

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

    @Override
    public void customize(Map<String, Object> hibernateProperties) {
        hibernateProperties.put(AvailableSettings.MULTI_TENANT_IDENTIFIER_RESOLVER, this);
    }
}

"在实际应用中,[字段currentTenant]将使用不同的范围(例如请求)或从适当设置了范围的其他bean获取值。"

原博客文章可在此处找到 here

博客文章的作者已将需要实现HibernatePropertiesCustomizer提出为一个issue,并且在未来可能无需实现它。"


0
也许您需要升级Hibernate版本到最新的4.X,并使用注释或方面来启动事务。

1
4.1.9是通过Maven打包的最新稳定版本。4.2作为CR可用,但即使使用它,我仍然收到相同的异常。 - Craige

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