Multi-Tenancy with Spring + Hibernate: "SessionFactory configured for multi-tenancy, but no tenant identifier specified"
Asked Answered
S

8

20

In a Spring 3 application, I'm trying to implement multi-tenancy via Hibernate 4's native MultiTenantConnectionProvider and CurrentTenantIdentifierResolver. I see that there was a problem with this in Hibernate 4.1.3, but I'm running 4.1.9 and still getting a similar exception:

   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

Below is the relevant code. In the MultiTenantConnectionProvider I've simply wrote some dumb code for now that just returns a new connection every time, and the CurrentTenantIdentifierResolver always returns the same ID at this point. Obviously this logic was to be implemented after I managed to get the connections to instantiate.

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

}

Can anybody see anything specifically wrong? This throws an exception as soon as a transaction is opened. It seems like the SessionFactory isn't opening the Session correctly, or the Session is simply ignoring the value returned by the CurrentTenantIdentifierResolver, which I believe was the issue in Hibernate 4.1.3; this was supposed to have been resolved.

Suspensoid answered 12/2, 2013 at 16:51 Comment(0)
S
11

Foreward: Although I accepted this answer which (will) contains code, please upvote Darren's answer if you think this was useful. He's the reason I was able to solve this at all.

Okay, so here we go....

As Darren pointed out, this is really an issue with SessionFactory's instantiating a Session improperly. If you were to instantiate the session manually, you have no issue. eg:

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

However, the @Transactional annotation causes the SessionFactory to open a session with sessionFactory.getCurrentSession(), which does not pull the tenant identifier from the CurrentTenantIdentifierResolver.

Darren suggested opening the Session manually in the DAO layer, but this means that each DAO method will have a locally scoped transaction. The better place to do this is on the service layer. Each service layer call (ie, doSomeLogicalTask()) may call multiple DAO methods. It makes sense that each of these should be bound to the same transaction, as they're logically related.

Furthermore, I didn't like the idea of duplicating code in each service layer method to create and manage a transaction. Instead, I used AOP to wrap each method in my service layer with with the advice to instantiate a new Session and handle the transaction. The aspect stores the current Session in a TheadLocal stack which can be accessed by the DAO layer for querying.

All of this work will allow the interfaces and implementations to stay identical to their bug-fixed counterparts, except one line in the DAO superclass that will get the Session from the ThreadLocal stack rather than the SessionFactory. This can be changed once the bug is fixed.

I will post the code shortly, once I clean it up a little. If anybody sees any problems with this, do feel free to discuss below.

Suspensoid answered 21/2, 2013 at 19:34 Comment(3)
May I know is this issue fixed in current latest hibernate version.Please share if possible your working code example.Councilwoman
Tested with Hibernate 4.2.6 it's working now.The issue resolved in this version.Councilwoman
Can I have your working example please, I am also using same pattern with @Transaction.Overslaugh
D
18

Are you using @Transactional anywhere in your code (ie mark a service or dao class/method)?
I was running into the same error until I commented out the @Transactional in my service class.
I think it's related to the default openSessionInThread behavior of Hibernate 4.

I also have hibernate configured without a custom implementation of the ConnectionProvider and TenantIdentifierResolver. I'm using the jndi-based approach, setting the hibernate.connection.datasource to java://comp/env/jdbc/, and then passing in the name of the jndi resource into my dao methods, which call

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

I'm still playing around to see if I can get a configuration working with @Transactional, but the jndi-based approach with the default session in thread behavior seems to be working now.

Dinnie answered 18/2, 2013 at 9:13 Comment(3)
I am using @Transactional, and I think you're spot-on with your theory. Can you provide some code samples of your Jndi solution? I'm going to play around with this today and see if I can get something working.Suspensoid
I worked up a solution based on your feedback here. I'm not 100% sold on it, but I'm using AOP to wrap my service layer methods with manual transaction handling. This will keep the public interface of my service layer and DAO classes clean. I'm also still utilizing the ConnectionProvider and TenantIdentifierResolver so when this problem gets patched in the future (which I belive is a bug), all my interfaces stay the same and I can just remove the aspect wrapping my service layer. I will post the relevant code soon.Suspensoid
@Suspensoid did you managed to get this working? can you share the code?Johnie
S
11

Foreward: Although I accepted this answer which (will) contains code, please upvote Darren's answer if you think this was useful. He's the reason I was able to solve this at all.

Okay, so here we go....

As Darren pointed out, this is really an issue with SessionFactory's instantiating a Session improperly. If you were to instantiate the session manually, you have no issue. eg:

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

However, the @Transactional annotation causes the SessionFactory to open a session with sessionFactory.getCurrentSession(), which does not pull the tenant identifier from the CurrentTenantIdentifierResolver.

Darren suggested opening the Session manually in the DAO layer, but this means that each DAO method will have a locally scoped transaction. The better place to do this is on the service layer. Each service layer call (ie, doSomeLogicalTask()) may call multiple DAO methods. It makes sense that each of these should be bound to the same transaction, as they're logically related.

Furthermore, I didn't like the idea of duplicating code in each service layer method to create and manage a transaction. Instead, I used AOP to wrap each method in my service layer with with the advice to instantiate a new Session and handle the transaction. The aspect stores the current Session in a TheadLocal stack which can be accessed by the DAO layer for querying.

All of this work will allow the interfaces and implementations to stay identical to their bug-fixed counterparts, except one line in the DAO superclass that will get the Session from the ThreadLocal stack rather than the SessionFactory. This can be changed once the bug is fixed.

I will post the code shortly, once I clean it up a little. If anybody sees any problems with this, do feel free to discuss below.

Suspensoid answered 21/2, 2013 at 19:34 Comment(3)
May I know is this issue fixed in current latest hibernate version.Please share if possible your working code example.Councilwoman
Tested with Hibernate 4.2.6 it's working now.The issue resolved in this version.Councilwoman
Can I have your working example please, I am also using same pattern with @Transaction.Overslaugh
D
3

Hibernate defines the CurrentTenantIdentifierResolver interface to help frameworks like Spring or Java EE to allow using the default Session instantiation mechanism (be it from an EntityManagerFactory).

So, the CurrentTenantIdentifierResolver must be set via a configuration property which is exactly where you went wrong because you didn't supply the right fully-qualified class name. The CurrentTenantIdentifierResolver implementation being CurrentTenantIdentifierResolverImpl, the hibernate.tenant_identifier_resolver has to be:

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

After you fix this, when the HibernateTransactionManager calls getSessionFactory().openSession(), Hibernate will use the CurrentTenantIdentifierResolverImpl to resolve the tenant identifier.

Detent answered 22/3, 2016 at 7:52 Comment(0)
H
2

Even though this might be an older topic, and the answer might be already taken care of. What I noticed is the following:

In your define the class CurrentTenantIdentifierResolverImpl:

public class CurrentTenantIdentifierResolverImpl implements CurrentTenantIdentifierResolver

But in your config you reference MultiTenantIdentifierResolverImpl:

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

Just pointing this out because I did the same mistake today, after that it all worked like a charm.

Hyperbaric answered 29/4, 2013 at 12:49 Comment(0)
S
1

I had a similar issue when my CurrentTenantIdentifierResolver implementation returned null for the resolveCurrentTenantIdentifier() method

Sedgewick answered 30/6, 2015 at 10:53 Comment(0)
T
1

I used Spring Boot 3.0.1 with Hibernates 6.1.6.Final and I solved this issue by implementing the interface 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);
    }
}

"In a real application [the field currentTenant] would either use a different scope (like request, for example) or get the value from some other bean that is appropriately scoped".

The original blog post is authored can be found here.

The fact that HibernatePropertiesCustomizer must be implemented is addressed by the author of the blog post as an issue to the hibernate team and may not be required in the future.

Travers answered 7/1, 2023 at 0:54 Comment(0)
L
0

Perhaps you need to upgrade version of hibernate to last 4.X and use annotation or aspects to start transaction

Loudspeaker answered 13/2, 2013 at 20:12 Comment(1)
4.1.9 is the latest stable version packaged via Maven. 4.2 is available as a CR, but even with that I get the same exception.Suspensoid
L
0

I've come across similar kind of issue, But not the same, Just posting maybe helpful to someone in future

Error

 org.hibernate.HibernateException: SessionFactory was not configured for multi-tenancy

Solution

We need to add what multi-tenancy strategy are we following by adding below to MultiTenantConnectionProvider

 public void customize(Map<String, Object> hibernateProperties) {
    hibernateProperties.put(AvailableSettings.MULTI_TENANT_CONNECTION_PROVIDER, this);
    hibernateProperties.put(AvailableSettings.MULTI_TENANT, MultiTenancyStrategy.SCHEMA);
  }
Libration answered 10/12, 2023 at 8:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.