Multi tenancy with spring data jpa and eclipselink
Asked Answered
N

2

5

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);
}
}
Nollie answered 27/10, 2014 at 13:21 Comment(2)
did you find solution for this?Gamaliel
Yes. The solution is based on eclipse-link specific handling of BindCallCustomParameter that is added as tenant holder to EM property map.Nollie
N
2

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

}

Nollie answered 7/3, 2015 at 20:33 Comment(3)
So this is all you need to have working multitenancy with Eclipselink and Spring Data? There is no need for custom Repositories anymore?Hymnology
Can you set tentant id with custom repositories per request?Nollie
Can you please share, what EM property did you use?Mink
S
1

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}
Scully answered 20/8, 2018 at 23:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.