![](/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.