Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Hibernate multitenancy: change tenant in session

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?

like image 749
Bas Dalenoord Avatar asked Jun 10 '15 13:06

Bas Dalenoord


People also ask

Does hibernate support Multitenancy?

Summary. Spring and Hibernate support multitenancy. We can implement two strategies using Spring Data JPA and Hibernate: separate database and separate schema.

What is multi-tenant mode?

Multitenancy is a reference to the mode of operation of software where multiple independent instances of one or multiple applications operate in a shared environment. The instances (tenants) are logically isolated, but physically integrated.

What is Multitenancy in hibernate?

Multitenancy allows multiple clients or tenants use a single resource or, in the context of this article, a single database instance. The purpose is to isolate the information each tenant needs from the shared database. In this tutorial, we'll introduce various approaches to configuring multitenancy in Hibernate 5.


2 Answers

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
    }
}
like image 97
Bas Dalenoord Avatar answered Sep 30 '22 00:09

Bas Dalenoord


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.

like image 39
Dudi Patimer Avatar answered Sep 30 '22 00:09

Dudi Patimer