tenant tenancy multi java spring hibernate multi-tenant

java - tenant - Multi-tenancy con Spring+Hibernate: "SessionFactory configurado para multi-tenancy, pero no se ha especificado ningĂșn identificador de arrendatario"



multi tenant spring boot (6)

En una aplicación Spring 3, estoy tratando de implementar la tenencia múltiple a través del MultiTenantConnectionProvider nativo de Hibernate 4 y CurrentTenantIdentifierResolver . Veo que hubo un problema con esto en Hibernate 4.1.3 , pero estoy ejecutando 4.1.9 y aún obtengo una excepción similar:

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

A continuación se muestra el código correspondiente. En el MultiTenantConnectionProvider , simplemente escribí un código estúpido por ahora que solo devuelve una nueva conexión cada vez, y el CurrentTenantIdentifierResolver siempre devuelve la misma ID en este punto. Obviamente, esta lógica se iba a implementar después de que lograra que las conexiones se instanciaran.

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; } }

¿Alguien puede ver algo específicamente mal? Esto lanza una excepción tan pronto como se abre una transacción. Parece que SessionFactory no está abriendo la Session correctamente, o que Session simplemente ignora el valor devuelto por el CurrentTenantIdentifierResolver , que creo que era el problema en Hibernate 4.1.3; se suponía que esto había sido resuelto.


¿Está utilizando @Transactional en algún lugar de su código (es decir, marque un servicio o clase / método dao)? Estaba corriendo en el mismo error hasta que comenté el @Transactional en mi clase de servicio. Creo que está relacionado con el comportamiento predeterminado de openSessionInThread de Hibernate 4.

También tengo la configuración de hibernación sin una implementación personalizada de ConnectionProvider y TenantIdentifierResolver. Estoy usando el enfoque basado en jndi, configurando hibernate.connection.datasource en java: // comp / env / jdbc /, y luego pasando el nombre del recurso jndi a mis métodos dao, que llaman sessionFactory.withOptions ( ) .tenantIdentifier (tenant) .openSession ();

Todavía estoy jugando para ver si puedo obtener una configuración que funcione con @Transactional, pero el enfoque basado en jndi con la sesión predeterminada en el comportamiento de subprocesos parece estar funcionando ahora.


A pesar de que este podría ser un tema más antiguo, y la respuesta ya podría haber sido resuelta. Lo que noté es lo siguiente:

En su definición de la clase CurrentTenantIdentifierResolverImpl:

public class CurrentTenantIdentifierResolverImpl implements CurrentTenantIdentifierResolver

Pero en su configuración usted hace referencia a MultiTenantIdentifierResolverImpl:

<prop key="hibernate.tenant_identifier_resolver">com.afflatus.edu.thoth.context.MultiTenantIdentifierResolverImpl</prop>

Solo señalé esto porque hoy cometí el mismo error, después de eso todo funcionó a la perfección.


Como expliqué en este artículo , Hibernate define la interfaz CurrentTenantIdentifierResolver para ayudar a los marcos como Spring o Java EE a permitir el uso del mecanismo de EntityManagerFactiry instancias de Session predeterminado (ya sea desde un EntityManagerFactiry ).

Por lo tanto, el CurrentTenantIdentifierResolver debe configurarse a través de una propiedad de configuración que es exactamente donde salió mal porque no proporcionó el nombre de clase completo y correcto. La implementación de CurrentTenantIdentifierResolver es CurrentTenantIdentifierResolverImpl , el hibernate.tenant_identifier_resolver tiene que ser:

<prop key="hibernate.tenant_identifier_resolver">com.afflatus.edu.thoth.context.CurrentTenantIdentifierResolverImpl</prop>

Después de solucionar este problema, cuando HibernateTransactionManager llama a getSessionFactory().openSession() , Hibernate utilizará el CurrentTenantIdentifierResolverImpl para resolver el identificador del inquilino.


Quizás necesite actualizar la versión de hibernación a la última versión 4.X y usar anotaciones o aspectos para iniciar la transacción


Tuve un problema similar cuando mi implementación CurrentTenantIdentifierResolver devolvió un valor nulo para el método resolverCurrentTenantIdentifier ()


Adelante: aunque acepté esta respuesta que (contendrá) código, por favor, vote por favor la respuesta de Darren si cree que esto fue útil. Él es la razón por la que pude resolver esto en absoluto.

Está bien, así que aquí vamos ...

Como señaló Darren , este es realmente un problema con SessionFactory que crea una sesión de forma incorrecta. Si tuviera que crear una instancia de la sesión manualmente, no tiene ningún problema. p.ej:

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

Sin embargo, la anotación @Transactional hace que SessionFactory abra una sesión con sessionFactory.getCurrentSession() , que no sessionFactory.getCurrentSession() el identificador de inquilino del CurrentTenantIdentifierResolver .

Darren sugirió abrir la sesión manualmente en la capa DAO, pero esto significa que cada método DAO tendrá una transacción de ámbito local. El mejor lugar para hacer esto es en la capa de servicio. Cada llamada de la capa de servicio (es decir, doSomeLogicalTask() ) puede llamar a múltiples métodos DAO. Tiene sentido que cada uno de estos deba estar vinculado a la misma transacción, ya que están relacionados lógicamente.

Además, no me gustaba la idea de duplicar código en cada método de capa de servicio para crear y administrar una transacción. En su lugar, usé AOP para envolver cada método en mi capa de servicio con el consejo de crear una nueva Session y manejar la transacción. El aspecto almacena la Session actual en una pila TheadLocal que puede acceder la capa DAO para realizar consultas.

Todo este trabajo permitirá que las interfaces y las implementaciones permanezcan idénticas a sus contrapartes de corrección de errores, excepto una línea en la superclase DAO que obtendrá la Session de la pila ThreadLocal lugar de SessionFactory . Esto se puede cambiar una vez que se solucione el error.

Voy a publicar el código en breve, una vez que lo limpie un poco. Si alguien ve algún problema con esto, siéntase libre de discutir a continuación.