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);
}
}
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);
}
}
Disclaimer: This does not answer the above query but provides an alternative.
Using bytecode instrumentation, I have created a java example on Multi-Tenancy (Table per Tenant) with Eclipse Link and Spring Data. This idea is chosen to utilize the complete power of Spring Data.
One can execute MultiTenantTest to see it working.
The idea is open-sourced and is available at Maven Central
Steps:
1.Include dependency
<dependency>
<groupId>org.bitbucket.swattu</groupId>
<artifactId>jpa-agent</artifactId>
<version>2.0.2</version>
</dependency>
2.Create a class as shown below. Package, Class and method has to be exactly same.
package org.swat.jpa.base;
import javax.persistence.EntityManager;
public class EntityManagerFactoryListener {
/**
* This method is called by JPA Agent.
*
* @param entityManager the entity manager
*/
public static void afterCreateEntityManager(EntityManager entityManager) {
//Business logic to set appropriate values in entityManager
}
}
3.Add javaagent when starting java
-javaagent:{path-to-jpa-agent-jar}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With