Multi Tenancy in Spring - Partitioned Data Approach

25 Views Asked by At

This is not a question, but a summary of the steps required to address multi tenancy with spring (the partitioned approach where every table will have a tenant id column). I hope this helps others facing issues or looking for sample code ...

Two things to consider here -

  1. The tenant id will be set in the header and passed with every http request.
  2. I have enabled spring data rest in my application.

Firstly we need an determine the tenant for each http request. We can do that by intercepting the http request before it is processed, determine the tenant and store it until the request is completely processed and clear the tenant before the next http request is processed.

import jakarta.validation.constraints.NotNull;

@FunctionalInterface
public interface TenantResolver<T> {

    String resolveTenantId(@NotNull T object);
}

Tenant context is the class which will store the tenant Id once it is identified by intercepting http requests.

public class TenantContext {

    private static final ThreadLocal<String> tenantId = new InheritableThreadLocal<>();

    public static void setTenantId(String tenant) {
        tenantId.set(tenant);
    }

    public static String getTenantId() {
        return tenantId.get();
    }

    public static void clear() {
        tenantId.remove();
    }
}

HTTPHeaderTenantResolver implements TenantResolver. X-TenantId is the name of the header which contains the tenant id and passed along with every http request.

import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.constraints.NotNull;
import org.springframework.stereotype.Component;

@Component
public class HttpHeaderTenantResolver implements TenantResolver<HttpServletRequest> {

    @Override
    public String resolveTenantId(@NotNull HttpServletRequest request) {
        return request.getHeader("X-TenantId");
    }
}

We then need to intercept each request by implementing HandlerInterceptor. In the preHandle() method, we resolve the tenant id and store it in the TenantContext class. In postHandle() and afterCompletion(), we clear the tenant id in TenantContext so we are ready to process new http requests.

@Component 
@RequiredArgsConstructor 
public class HttpRequestInterceptor implements HandlerInterceptor {
        
            private final HttpHeaderTenantResolver tenantResolver;
        
            @Override
            public boolean preHandle(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response,
                                     @NotNull Object handler) throws Exception {
        
                var tenantId = tenantResolver.resolveTenantId(request);
                TenantContext.setTenantId(tenantId);
                return true;
            }
        
            @Override
            public void postHandle(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response,
                                   @NotNull Object handler, ModelAndView modelAndView) throws Exception {
        
                TenantContext.clear();
            }
        
            @Override
            public void afterCompletion(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response,
                                        @NotNull Object handler, Exception ex) throws Exception {
        
                TenantContext.clear();
            } 
    }

We then need to register HttpRequestInterceptor with Spring. We do that by implementing addInterceptors method of WebMvcConfigurer.

@Configuration
@RequiredArgsConstructor
public class WebConfigurer implements WebMvcConfigurer {

    private final HttpRequestInterceptor httpRequestInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(httpRequestInterceptor);
    }
}

With this, spring starts intercepting every http request and will try to resolve the tenant id. We now have to let hibernate know of the current tenant id so that hibernate can use the tenant id in all sql statements. We can do that by implementing the interfaces CurrentTenantIdentifierResolver and HibernatePropertiesCustomizer.

@Component
public class JPATenantIdentifierResolver implements CurrentTenantIdentifierResolver<String>, HibernatePropertiesCustomizer {

    @Override
    public String resolveCurrentTenantIdentifier() {

        return (TenantContext.getTenantId() == null ? "" : TenantContext.getTenantId());
    }

    @Override
    public boolean validateExistingCurrentSessions() {
        return false;
    }

    @Override
    public void customize(Map<String, Object> hibernateProperties) {
        hibernateProperties.put(AvailableSettings.MULTI_TENANT_IDENTIFIER_RESOLVER, this);
    }
}

Now hibernate is aware of the current tenant and it will add the tenant id in all sql statements.

Lastly, as I have enabled spring data rest, we will require one additional step. Spring does not intercept data rest urls by default. We need to define an addition bean to achieve this step.

@Configuration
public class JPARequestInterceptor {

    @Bean
    public MappedInterceptor addJPARestHttpInterceptor() {
        return new MappedInterceptor(
                null,  // => maps to any repository
                new HttpRequestInterceptor(new HttpHeaderTenantResolver())
        );
    }
}

We can then define our entities. @TenantId annotation is the hibernate annotation and should be there in all entities.

public class Instance {

    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    @JdbcTypeCode(SqlTypes.VARCHAR)
    private UUID id;

    @TenantId
    private String tenant;

    private String name;
    private LocalDate startDate;
    private LocalDate validUntil;
    private String instanceStatus;
}

Below are some links which will provide more information on multi tenancy -

https://www.youtube.com/watch?v=pG-NinTx4O4&t=1164s

MappedInterceptor Bean Vs WebMvcConfigurer addInterceptors. What is the correct (modern) way for adding Spring HandlerInterceptor?

https://github.com/spring-projects/spring-data-rest/issues/1522

0

There are 0 best solutions below