![](/img/trans.png)
[英]Multi-tenancy in Spring Boot + Hibernate5 - Schema per tenant
[英]Multi-Tenancy with Spring + Hibernate: "SessionFactory configured for multi-tenancy, but no tenant identifier specified"
在 Spring 3 应用程序中,我试图通过 Hibernate 4 的本地MultiTenantConnectionProvider和CurrentTenantIdentifierResolver实现多租户。 我看到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。 显然这个逻辑是在我设法让连接实例化之后实现的。
<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>
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;
}
}
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 中的问题; 这应该已经解决了。
您是否在代码中的任何地方使用@Transactional
(即标记服务或 dao 类/方法)?
在我注释掉服务类中的@Transactional
之前,我@Transactional
了同样的错误。
我认为这与 Hibernate 4 的默认 openSessionInThread 行为有关。
我还配置了休眠,但没有自定义ConnectionProvider
和TenantIdentifierResolver
。 我正在使用基于 jndi 的方法,将hibernate.connection.datasource设置为 java://comp/env/jdbc/,然后将 jndi 资源的名称传递到我的 dao 方法中,该方法调用
sessionFactory.withOptions().tenantIdentifier(tenant).openSession();
我仍在尝试查看是否可以使用@Transactional
进行配置,但是在线程行为中使用默认会话的基于 jndi 的方法现在似乎正在起作用。
前言:虽然我接受了这个(将)包含代码的答案,但如果您认为这有用,请支持Darren 的答案。 他是我能够解决这个问题的原因。
好的,所以我们开始......
正如达伦指出的那样,这确实是 SessionFactory 不正确地实例化 Session 的一个问题。 如果您要手动实例化会话,则没有问题。 例如:
sessionFactory.withOptions().tenantIdentifier(tenant).openSession();
但是,@ @Transactional
注释会导致 SessionFactory 使用sessionFactory.getCurrentSession()
打开一个会话,这不会从CurrentTenantIdentifierResolver
提取租户标识符。
Darren 建议在 DAO 层手动打开 Session,但这意味着每个 DAO 方法都会有一个本地范围的事务。 最好的地方是在服务层。 每个服务层调用(即doSomeLogicalTask()
)可能会调用多个 DAO 方法。 由于它们在逻辑上相关,因此它们中的每一个都应该绑定到同一个事务是有道理的。
此外,我不喜欢在每个服务层方法中复制代码来创建和管理事务的想法。 相反,我使用 AOP 将每个方法包装在我的服务层中,并提供建议以实例化新Session
并处理事务。 该方面将当前Session
存储在TheadLocal
堆栈中,DAO 层可以访问该堆栈以进行查询。
所有这些工作将允许接口和实现与其修复了错误的对应物保持相同,除了 DAO 超类中的一行将从ThreadLocal
堆栈而不是SessionFactory
获取Session
。 修复错误后,可以更改此设置。
一旦我稍微清理一下,我将很快发布代码。 如果有人看到这方面的任何问题,请随时在下面讨论。
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
来解析租户标识符。
即使这可能是一个较旧的主题,并且答案可能已经得到解决。 我注意到的是以下内容:
在您定义类 CurrentTenantIdentifierResolverImpl 中:
public class CurrentTenantIdentifierResolverImpl implements CurrentTenantIdentifierResolver
但是在您的配置中,您引用了 MultiTenantIdentifierResolverImpl:
<prop key="hibernate.tenant_identifier_resolver">com.afflatus.edu.thoth.context.MultiTenantIdentifierResolverImpl</prop>
只是指出这一点,因为我今天犯了同样的错误,之后一切都像魅力一样。
也许您需要将 hibernate 的版本升级到最新的 4.X 并使用注解或方面来启动事务
当我的 CurrentTenantIdentifierResolver 实现为 resolveCurrentTenantIdentifier() 方法返回 null 时,我遇到了类似的问题
我将 Spring Boot 3.0.1 与 Hibernates 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
] 要么使用不同的 scope(例如请求),要么从其他适当范围的 bean 获取值”。
可以在此处找到原创博客文章。
必须实施HibernatePropertiesCustomizer
的事实已由博文作者作为 hibernate 团队的问题解决,将来可能不再需要。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.