4 Mayıs 2021 Salı

SpringData Multitenancy

Giriş
Multitenancy için 3 yöntem var. Bunlar şöyle
Separate database 
Separate schema
Shared schema
Multitenancyi için spring ve hibernate'i ayrı ayrı ayarlamak lazım

Not : Webflux MongoDB için örnek burada

Spring
Elimizde şöyle bir kod olsun
public abstract class TenantContext {
 
  public static final String DEFAULT_TENANT_ID = "public";
  private static ThreadLocal<String> currentTenant = new ThreadLocal<>();
 
  public static void setCurrentTenant(String tenant) {
    currentTenant.set(tenant);
  }
 
  public static String getCurrentTenant() {
    return currentTenant.get();
  }
 
  public static void clear() {
    currentTenant.remove();
  }
}
Bu kodu dolduracak bir interceptor yazılır. Açıklaması şöyle
In the earlier versions of Spring Boot, we could extend the org.springframework.web.servlet.handler.HandlerInterceptorAdapter class, but in newer versions, the class is deprecated,...
Yani org.springframework.web.servlet.AsyncHandlerInterceptor kullanılır. Şöyle yaparız
@Component
public class TenantRequestInterceptor implements AsyncHandlerInterceptor {
        
  private SecurityDomain securityDomain;
        
  public TenantRequestInterceptor(SecurityDomain securityDomain) {
    this.securityDomain = securityDomain;
  }
 
  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) {
    return Optional.ofNullable(request)
      .map(req -> securityDomain.getTenantIdFromJwt(req))
      .map(tenant -> setTenantContext(tenant))
      .orElse(false);
  }
 
  @Override
  public void postHandle(HttpServletRequest request, HttpServletResponse response,
Object handler, ModelAndView modelAndView) {
    TenantContext.clear();
  }
         
  private boolean setTenantContext(String tenant) {
    TenantContext.setCurrentTenant(tenant);
    return true;
  }
}
Interceptor eklenir
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
 
  @Autowired
  private TenantRequestInterceptor tenantInterceptor;
        
  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(tenantInterceptor).addPathPatterns("/**");
  }      
}

1. Separate database
Hibernate tarafından sağlanan AbstractMultiTenantConnectionProvider sınıfından kalıtan yeni bir ConnectionProvider yazılır. Şeklen şöyle



Bu sınıfı kodlarken şu metodlar override edilir
protected ConnectionProvider getAnyConnectionProvider();
protected ConnectionProvider selectConnectionProvider(String tenantIdentifier);
Örnek
Şöyle yaparız
public class SchemaMultiTenantConnectionProvider extends 
  AbstractMultiTenantConnectionProvider {
        
  public static final String HIBERNATE_PROPERTIES_PATH = "/hibernate-%s.properties";
  private final Map<String, ConnectionProvider> connectionProviderMap;
 
  public SchemaMultiTenantConnectionProvider() {
    this.connectionProviderMap = new HashMap<String, ConnectionProvider>();
  }
        
  @Override
  protected ConnectionProvider getAnyConnectionProvider() {
    return getConnectionProvider(TenantContext.DEFAULT_TENANT_ID);
  }
 
   @Override
   protected ConnectionProvider selectConnectionProvider(String tenantIdentifier) {
     return getConnectionProvider(tenantIdentifier);
  }
        
  private ConnectionProvider getConnectionProvider(String tenantIdentifier) {
    return Optional.ofNullable(tenantIdentifier)
                   .map(connectionProviderMap::get)
                   .orElseGet(() -> createNewConnectionProvider(tenantIdentifier));
  }
 
   private ConnectionProvider createNewConnectionProvider(String tenantIdentifier) {
     return Optional.ofNullable(tenantIdentifier)
                    .map(this::createConnectionProvider)
                    .map(connectionProvider -> {
                      connectionProviderMap.put(tenantIdentifier, connectionProvider);
                      return connectionProvider;
                     })
                     .orElseThrow(() -> 
                        new ConnectionProviderException(
                          String.format("Cannot create new connection provider 
                                         for tenant: %s", tenantIdentifier))
                    );
  }
        
  private ConnectionProvider createConnectionProvider(String tenantIdentifier) {
    return Optional.ofNullable(tenantIdentifier)
                   .map(this::getHibernatePropertiesForTenantId)
                   .map(this::initConnectionProvider)
                   .orElse(null);
  }
        
  private Properties getHibernatePropertiesForTenantId(String tenantId) {
    try {
      Properties properties = new Properties();
      properties.load(getClass().getResourceAsStream(
        String.format(HIBERNATE_PROPERTIES_PATH, tenantId)));
      return properties;
    } catch (IOException e) {
      throw new RuntimeException(
     String.format("Cannot open hibernate properties: %s", HIBERNATE_PROPERTIES_PATH));
    }
  }
 
   private ConnectionProvider initConnectionProvider(Properties hibernateProperties) {
     DriverManagerConnectionProviderImpl connectionProvider = 
      new DriverManagerConnectionProviderImpl();
     connectionProvider.configure(hibernateProperties);
     return connectionProvider;
   }
}
Açıklaması şöyle
The only thing we have to implement in this class are the methods getAnyConnectionProvider() and selectConnectionProvider(String tenantId).

The first method sets up a connection to the database when the tenantId is not set. This happens when the application is starting. Validating whether the tenant is set can be implemented in AsyncHandlerInterceptor on the Spring side and throws an exception when it’s not set or is incorrect.

The second method is responsible for returning the appropriate ConnectionProvider for the indicated tenantId. The solution assumes that we collect ConnectionProvider on a map so as not to create a new one every time. If it’s not on the map yet, then we create a new one and add it to the map. Of course, this can be moved to the classic cache, where you can additionally manage lifetime (TTL).



Hiç yorum yok:

Yorum Gönder