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?