6

We're developing a SaaS solution for several consumers. This solution is based on Spring, Wicket and Hibernate. Our database contains data from several customers. We've decided to model the database as follows:

  • public
    Shared data between all customers, for example user accounts as we do not know which customer a user belongs to
  • customer_1
  • customer_2
  • ...

To work with this setup we use a multi-tenancy setup with the following TenantIdentifierResolver:

public class TenantProviderImpl implements CurrentTenantIdentifierResolver {
    private static final ThreadLocal<String> tenant = new ThreadLocal<>();

    public static void setTenant(String tenant){
        TenantProviderImpl.tenant.set(tenant);
    }

    @Override
    public String resolveCurrentTenantIdentifier() {
        return tenant.get();
    }

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

    /**
     * Initialize a tenant by storing the tenant identifier in both the HTTP session and the ThreadLocal
     *
     * @param   String  tenant  Tenant identifier to be stored
     */
    public static void initTenant(String tenant) {
        HttpServletRequest req = ((ServletWebRequest) RequestCycle.get().getRequest()).getContainerRequest();
        req.getSession().setAttribute("tenant", tenant);
        TenantProviderImpl.setTenant(tenant);
    }
}

The initTenant method is called by a servlet filter for every request. This filter is processed before a connection is opened to the database.

We've also implemented a AbstractDataSourceBasedMultiTenantConnectionProviderImpl which is set as our hibernate.multi_tenant_connection_provider. It issues a SET search_path query before every request. This works like charm for requests passing through the servlet filter described above.

And now for our real problem: We've got some entrypoints into our application which do not pass the servlet filter, for instance some SOAP-endpoints. There are also timed jobs that are executed which do not pass the servlet filter. This proves to be a problem.

The Job/Endpoint receives a value somehow which can be used to identify which customer should be associated with the Job/Endpoint-request. This unique value is often mapped in our public database schema. Thus, we need to query the database before we know which customer is associated. Spring therefore initializes a complete Hibernate session. This session has our default tenant ID and is not mapped to a specific customer. However, after we've resolved the unique value to a customer we want the session to change the tenant identifier. This seems to not be supported though, there is no such thing as a HibernateSession.setTenantIdentifier(String) whereas there is a SharedSessionContract.getTenantIdentifier().

We thought we had a solution in the following method:

org.hibernate.SessionFactory sessionFactory = getSessionFactory();
org.hibernate.Session session = null;
try
{
    session = getSession();
    if (session != null)
    {
       if(session.isDirty())
       {
          session.flush();
       }
       if(!session.getTransaction().wasCommitted())
       {
          session.getTransaction().commit();
       }

       session.disconnect();
       session.close();
       TransactionSynchronizationManager.unbindResource(sessionFactory);
    }
}
catch (HibernateException e)
{
    // NO-OP, apparently there was no session yet
}
TenantProviderImpl.setTenant(tenant);
session = sessionFactory.openSession();
TransactionSynchronizationManager.bindResource(sessionFactory, new SessionHolder(session));
return session;

This method however does not seem to work in the context of a Job/Endpoint and leads to HibernateException such as "Session is closed!" or "Transaction not succesfully started".

We're a bit lost as we've been trying to find a solution for quite a while now. Is there something we've misunderstood? Something we've misinterpreted? How can we fix the problem above?

Recap: HibernateSession-s not created by a user request but rather by a timed job or such do not pass our servlet filter and thus have no associated tenant identifier before the Hibernate session is started. They have unique values which we can translate to a tenant identifier by querying the database though. How can we tell an existing Hibernate session to alter it's tenant identifier and thus issue a new SET search_path statement?

Bas Dalenoord
  • 509
  • 6
  • 17
  • It`s a bit late but this seems relevant https://hibernate.atlassian.net/browse/HHH-9766 – chimmi Dec 25 '15 at 09:13
  • It seems you're right, the ticket states this is in fact unsupported behavior as of yet. We've found a workaround which I'll describe below... – Bas Dalenoord Dec 28 '15 at 06:50
  • I am very late here but would AOP have helped you here? I am not sure, but before executing SOAP endpoints (contains the tenantId in path) and quartz scheduler (I think there is a way we can pass data to scheduler, there we could have passed tenantId) could have helped here? – Bilbo Baggins Apr 11 '17 at 12:09
  • I'm not sure if AOP might have helped, I'm not that familiar with the possibilities of AOP. The issue is related to a design choice we made by using the OpenSessionInViewFilter, which' goal it is to prevent using multiple sessions in a single request thread. This combined with the fact that Hibernate does not allow switching tenants in an open session means that it is not easily achieved. We've since built another Hibernate-based application without the OpenSessionInViewFilter, where this wasn't a problem at all. – Bas Dalenoord Apr 12 '17 at 12:48
  • This solution from @Ermintar worked well for me. https://stackoverflow.com/questions/46847059/change-multitenancy-sessions-manually – TheJeff Nov 16 '20 at 21:15

3 Answers3

3

We've never found a true solution for this problem, but chimmi linked to a Jira-ticket were others have requested such a feature: https://hibernate.atlassian.net/browse/HHH-9766

As per this ticket, the behavior we want is currently unsupported. We've found a workaround though, as the number of times we actually want to use this feature is limited it is feasible for us to run these operations in separate threads using the default java concurrency implementation.

By running the operation in a separate thread, a new session is created (as the session is threadbound). It is very important for us to set the tenant to a variable shared across threads. For this we have a static variable in the CurrentTenantIdentifierResolver.

For running an operation in a separate thread, we implement a Callable. These callables are implemented as Spring-beans with scope prototype so a new instance is created for each time it is requested (autowired). We've implemented our own abstract implementation of a Callable which finalizes the call()-method defined by the Callable interface, and the implementation starts a new HibernateSession. The code looks somewhat like this:

public abstract class OurCallable<TYPE> implements Callable<TYPE> {
    private final String tenantId;

    @Autowired
    private SessionFactory sessionFactory;

    // More fields here

    public OurCallable(String tenantId) {
        this.tenantId = tenantId;
    }

    @Override
    public final TYPE call() throws Exception {
        TenantProvider.setTenant(tenantId);
        startSession();

        try {
            return callInternal();
        } finally {
            stopSession();
        }
    }

    protected abstract TYPE callInternal();

    private void startSession(){
        // Implementation skipped for clarity
    }

    private void stopSession(){
        // Implementation skipped for clarity
    }
}
Community
  • 1
  • 1
Bas Dalenoord
  • 509
  • 6
  • 17
3

Another workaround I've found thanks to @bas-dalenoord comment regarding OpenSessionInViewFilter/OpenEntityManagerInViewInterceptor which led me to this direction, is to disable this interceptor.

This can be achieved easily by setting spring.jpa.open-in-view=false either in the application.properties or environment-variable.

OpenEntityManagerInViewInterceptor binds a JPA EntityManager to the thread for the entire processing of the request and in my case it's redundant.

Dudi Patimer
  • 181
  • 1
  • 4
  • this could be better documented... I lost hours changing and playing with things, researching and punching my desk till I found your anware... thank you dude! – Frohlich Jul 20 '20 at 17:31
0

Another workaround is to break the request that needs to make DB calls on behalf of 2 different tenants into 2 separate requests. First the client ask for his associated tenant in the system, and then creates a new request with the given tenant as a parameter. IMO, until (and if) the feature will be supported, it's a relatively clean alternative.

Alon Segal
  • 818
  • 9
  • 20