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;
}
}
@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
edilirprotected ConnectionProvider getAnyConnectionProvider();
protected ConnectionProvider selectConnectionProvider(String tenantIdentifier);
Örnek
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;
}
}
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).