Search code examples
eclipselinkspring-data-jpamulti-tenant

Multi tenancy with spring data jpa and eclipselink


I'm trying to add multi tenancy support to my Spring data jpa repositories. I would like to set dynamically the tenant id per request, but it does not work for the custom finder findBy* methods on repository. I've followed this guide: http://codecrafters.blogspot.sk/2013/03/multi-tenant-cloud-applications-with.html

My repository looks like this:

public interface CountryRepository extends PagingAndSortingRepository<Country, Long> {

    Country findByName(String name);
    Country findByIsoCountryCode(String isoCountryCode);

}

I'm getting the error below when I call any of the custom finder findBy* methods on the repository interface:

javax.persistence.PersistenceException: Exception [EclipseLink-6174] (Eclipse Persistence Services - 2.5.2.v20140319-9ad6abd): org.eclipse.persistence.exceptions.QueryException
Exception Description: No value was provided for the session property [eclipselink.tenant-id]. This exception is possible when using additional criteria or tenant discriminator columns without specifying the associated contextual property. These properties must be set through Entity Manager, Entity Manager Factory or persistence unit properties. If using native EclipseLink, these properties should be set directly on the session.
Query: ReadAllQuery(referenceClass=Country sql="SELECT ID, TENANT_ID, CONTINENT, CREATED_BY, CREATED_DATETIME, CURRENCY, INDEPENDENTFROM, ISOCOUNTRYCODE, LONGNAME, MODIFIED_BY, MODIFIED_DATETIME, NAME, POPULATION, REC_VERSION FROM COUNTRY WHERE ((NAME = ?) AND (TENANT_ID = ?))")
    at org.eclipse.persistence.internal.jpa.QueryImpl.getSingleResult(QueryImpl.java:547)
    at org.eclipse.persistence.internal.jpa.EJBQueryImpl.getSingleResult(EJBQueryImpl.java:400)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at org.springframework.orm.jpa.SharedEntityManagerCreator$DeferredQueryInvocationHandler.invoke(SharedEntityManagerCreator.java:360)
    at com.sun.proxy.$Proxy56.getSingleResult(Unknown Source)
    at org.springframework.data.jpa.repository.query.JpaQueryExecution$SingleEntityExecution.doExecute(JpaQueryExecution.java:197)
    at org.springframework.data.jpa.repository.query.JpaQueryExecution.execute(JpaQueryExecution.java:74)
    at org.springframework.data.jpa.repository.query.AbstractJpaQuery.doExecute(AbstractJpaQuery.java:97)
    at org.springframework.data.jpa.repository.query.AbstractJpaQuery.execute(AbstractJpaQuery.java:88)
    at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.doInvoke(RepositoryFactorySupport.java:421)
    at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.invoke(RepositoryFactorySupport.java:381)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:98)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:262)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:95)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:136)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodIntercceptor.invoke(CrudMethodMetadataPostProcessor.java:111)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:207)
    at com.sun.proxy.$Proxy52.findByName(Unknown Source) 

I assume that spring data generates the implementation of those custom finder findBy* methods at the initialization phase and put them into a cache with the current entity manager without a tenant id set on it and I am not able to set/change the tenant id on this cached entity manager. I'm trying to change the tenant id on the entity manager dynamically per request, so the question is how can I change/set the tenant id on that cached entity manager, which is used when I call any of the custom finder findBy* methods.

Here is my multitenant querydsl repository implementation:

public class MultiTenantQueryDslJpaRepository<T, ID extends Serializable> extends QueryDslJpaRepository<T, ID> {
private final CurrentTenantResolver currentTenantResolver;
protected final EntityManager entityManager;

public MultiTenantQueryDslJpaRepository(JpaEntityInformation<T, ID> entityInformation, EntityManager entityManager, CurrentTenantResolver currentTenantResolver) {
    this(entityInformation, entityManager, SimpleEntityPathResolver.INSTANCE, currentTenantResolver);
}

public MultiTenantQueryDslJpaRepository(JpaEntityInformation<T, ID> entityInformation, EntityManager entityManager, EntityPathResolver resolver, CurrentTenantResolver currentTenantResolver) {
    super(entityInformation, entityManager, resolver);
    this.currentTenantResolver = currentTenantResolver;
    this.entityManager = entityManager;
}

protected void setCurrentTenant() {
    entityManager.setProperty(PersistenceUnitProperties.MULTITENANT_PROPERTY_DEFAULT, currentTenantResolver.getCurrentTenantId());
}

@Override
protected JPQLQuery createQuery(final Predicate... predicate) {
    setCurrentTenant();
    return super.createQuery(predicate);
}

@Override
public void delete(final T entity) {
    setCurrentTenant();
    super.delete(entity);
}

@Override
public T findOne(final ID id) {
    setCurrentTenant();
    return super.findOne(id);
}

@Override
public void deleteInBatch(final Iterable<T> entities) {
    setCurrentTenant();
    super.deleteInBatch(entities);
}

@Override
public void deleteAllInBatch() {
    setCurrentTenant();
    super.deleteAllInBatch();
}

@Override
public T getOne(final ID id) {
    setCurrentTenant();
    return super.getOne(id);
}

@Override
public boolean exists(final ID id) {
    setCurrentTenant();
    return super.exists(id);
}

@Override
protected TypedQuery<T> getQuery(final Specification<T> spec, final Sort sort) {
    setCurrentTenant();
    return super.getQuery(spec, sort);
}

@Override
public long count() {
    setCurrentTenant();
    return super.count();
}

@Override
protected TypedQuery<Long> getCountQuery(final Specification<T> spec) {
    setCurrentTenant();
    return super.getCountQuery(spec);
}

@Override
public <S extends T> S save(final S entity) {
    setCurrentTenant();
    return super.save(entity);
}
}

Solution

  • The solution is based on eclipse-link specific handling of BindCallCustomParameter that is added as tenant holder to EM property map.

    public class TenantHolder extends BindCallCustomParameter {
    
    private final TenantResolver tenantResolver;
    
    private String defaultTenant;
    
    public TenantHolder(String defaultTenant, TenantResolver tenantResolver) {
        this.defaultTenant = defaultTenant;
        this.tenantResolver = tenantResolver;
    }
    
    public String getDefaultTenant() {
        return defaultTenant;
    }
    
    @Override
    public void set(DatabasePlatform platform, PreparedStatement statement, int index, AbstractSession session) throws SQLException {
        String resolvedTenant = resolveTenant();
        platform.setParameterValueInDatabaseCall(resolvedTenant, statement, index, session);
    }
    
    private String resolveTenant() {
        return tenantResolver.resolveTenant(defaultTenant);
    }
    

    }