31 Mayıs 2023 Çarşamba

SpringData Multitenancy - Hibernate İle Shared Database, Separate Schemas

Giriş
Bu yöntem JPA sağlayıcısı olarak Hibernate kullanıyorsak işe yarar. Normalde Hibernate "separate database" veya "separate schema" yöntemini destekler. Açıklaması şöyle
In Hibernate, we will have 2 classes to implement:

org.hibernate.context.spi.CurrentTenantIdentifierResolver — which will define a tenantId for Hibernate, thanks to which it will know what resources to get to;

org.hibernate.engine.jdbc.connections.spi.AbstractMultiTenantConnectionProvider — will open a connection to resources based on the CurrentTenantIdentifierResolver tenant id returned.
Bu yazıda "separate schema" yöntemi anlatılıyor. Açıklaması şöyle
In this approach, all tenants share a single database, but each has its own schema. This pattern provides a balance between data isolation and resource optimization. With Hibernate, which Spring Boot uses by default for ORM, you can dynamically set the schema based on the tenant context.

For this approach, you can use a similar DataSource setup as the first approach. However, instead of having different data sources, you'll switch the Hibernate default schema dynamically. 
Kısaca
1. CurrentTenantIdentifierResolver arayüzünden kalıtan bir bean kodlarız. Bu bean'de  resolveCurrentTenantIdentifier() metodunu kodlarız ve Hibernate'in kullanmasını istediğimiz schema ismini döneriz.

CurrentTenantIdentifierResolver Sınıfı
Şu satırı dahil ederiz
import org.hibernate.context.spi.CurrentTenantIdentifierResolver;
resolveCurrentTenantIdentifier metodu
Örnek
Şöyle yaparız
public class TenantSchemaResolver implements CurrentTenantIdentifierResolver {
  @Override
  public String resolveCurrentTenantIdentifier() {
    return TenantContext.getTenantSchema(); // Get tenant's schema from tenant context
  }
  @Override
  public boolean validateExistingCurrentSessions() {
    return true;
  }
}
Örnek
Şöyle yaparız
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class TenantIdentifierResolver implements CurrentTenantIdentifierResolver {
        
  @Override
  public String resolveCurrentTenantIdentifier() {
    return Optional.ofNullable(TenantContext.getCurrentTenant())
                   .orElse(TenantContext.DEFAULT_TENANT_ID);
  }
 
  @Override
  public boolean validateExistingCurrentSessions() {
    return true;
  }
}
Açıklaması şöyle
- The resolveCurrentTenantIdentifier() method returns the tenantId for the tenant in the context.
- If we want Hibernate to validate all existing sessions for the indicated tenantId, then we return the value true in the validateExistingCurrentSessions() method.
TenantContext şöyle. Sadece ThreadLocal nesnesini barındırıyor
public 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();
  }      
}
Şimdi bir AsyncHandlerInterceptor lazım. Böylece her isteğin başında schemaId atanır ve istek sonunda silinir. Şö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;
  }
}
Bu AsyncHandlerInterceptor nesnesini Spring'e tanıtmak için şöyle yaparız
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
 
  @Autowired
  private TenantRequestInterceptor tenantInterceptor;
        
  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(tenantInterceptor).addPathPatterns("/**");
  }    
}
Ayarlar şöyle
spring.jpa.properties.hibernate.multiTenancy=

spring.jpa.properties.hibernate.tenant_identifier_resolver=

spring.jpa.properties.hibernate.multi_tenant_connection_provider=
Açıklaması şöyle
The first parameter is used to define what multitenancy strategy we are adopting. We have a choice of values ​​here:

NONE — default value — multitenancy is disabled. In this strategy, if we set a tenantId, Hibernate will throw an exception.
SCHEMA — for the separate schema strategy.
DATABASE — for the separate database strategy.
DISCRIMINATOR — for the shared schema strategy (not yet implemented in Hibernate. It was planned for Hibernate 5, but this issue is still open, so we need to wait.)
Açıklaması şöyle
The other two parameters point to the corresponding class implementations for CurrentTenantIdentifierResolver and AbstractMultiTenantConnectionProvider.
Ayrıca hibernate ve spring için bağlantı ayarlarını belirtmek gerekiyor. Şöyle yaparız
hibernate.connection.url=
hibernate.connection.username=
hibernate.connection.password=
spring.datasource.url=${hibernate.connection.url}
spring.datasource.username=${hibernate.connection.username}
spring.datasource.password=${hibernate.connection.password}


Hiç yorum yok:

Yorum Gönder