Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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);
}
}
like image 386
Ati Avatar asked Oct 19 '22 23:10

Ati


2 Answers

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);
}

}

like image 125
Ati Avatar answered Oct 29 '22 13:10

Ati


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}
like image 45
Swat Avatar answered Oct 29 '22 12:10

Swat