Multitenancy için 3 yöntem var. Bunlar şöyle
Separate databaseSeparate schemaShared 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.
@Componentpublic class TenantRequestInterceptor implements AsyncHandlerInterceptor {private SecurityDomain securityDomain;public TenantRequestInterceptor(SecurityDomain securityDomain) {this.securityDomain = securityDomain;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) {return Optional.ofNullable(request).map(req -> securityDomain.getTenantIdFromJwt(req)).map(tenant -> setTenantContext(tenant)).orElse(false);}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response,
Object handler, ModelAndView modelAndView) {TenantContext.clear();}private boolean setTenantContext(String tenant) {TenantContext.setCurrentTenant(tenant);return true;}}
Interceptor eklenir
@Configurationpublic class WebConfiguration implements WebMvcConfigurer {@Autowiredprivate TenantRequestInterceptor tenantInterceptor;@Overridepublic 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