Multitenancy یا چند مستاجری به چندین کلاینت اجازه میدهد از یک منبع استفاده کنند و اطلاعات اشتراکی بین کلاینت ها بصورت ایزوله استفاده شود در این بخش خواهیم دید چگونه میتوان در Hibernate 5 دیتابیس را برای Multitenancy کانفیگ و استفاده کرد
ابتدا وابستگی ها را به Pom اضافه میکنیم :
<dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> <version>5.2.12.Final</version> </dependency>
چون در این مثال از h2 استفاده میکنیم لازم است آن را هم به پروژه اضافه کنیم :
<dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>1.4.196</version> </dependency>
مفهوم Multitenancy در Hibernate :
همانطور که در توضیحات Hibernate آمده است برای سه هدف از Multitenancy استفاده میشود :
- جداسازی Schema : یک schema به ازای یک مستاجر در یک دیتابیس فیزیکی
- جداسازی دیتابیس : یک دیتابیس فیزیکی جداگانه برای هر مستاجر
- داده های تقسیم شده یا Partitioned (Discriminator) Data : داده ها برای هر مستاجر تفکیک شده اند که هنوز توسط Hibernate پشتیبانی نمیشود
هایبرنیت سعی دارد پیچیدگی ها را برای استفاده ساده کند برای همین دو اینترفیس را برای استفاده معرفی کرده است :
- MultiTenantConnectionProvider که برای هر مستاجر connection را فراهم میکند
- CurrentTenantIdentifierResolver که برای هر مستاجر Id آنرا شناسایی میکند
MultiTenantConnectionProvider :
interface MultiTenantConnectionProvider extends Service, Wrapped { Connection getAnyConnection() throws SQLException; Connection getConnection(String tenantIdentifier) throws SQLException; // ... }
* اگر هایبرنیت نتواند Connection ای با ID مستاجر پیدا کند از متد ()getAnyConnection استفاده میکند
برای پیاده سازی هایبرنیت دو کلاس را ارائه داده است :
اگر از اینترفیس DataSource استفاده کنیم میتوانیم کلاس DataSourceBasedMultiTenantConnectionProviderImpl را بکار ببریم
اگر از اینترفیس ConnectionProvider استفاده کنیم کلاس AbstractMultiTenantConnectionProvider برای پیاده سازی قابل استفاده است
CurrentTenantIdentifierResolver :
راه های زیادی برای شناسایی کردن مستاجر وجود دارد مثلا میتواند با استفاده از یک فایل تنظیمات اطلاعات شناسایی استخراج شود و یا از پارامتر path استفاده کند
ساختار این اینترفیس :
public interface CurrentTenantIdentifierResolver { String resolveCurrentTenantIdentifier(); boolean validateExistingCurrentSessions(); }
هایبرنیت ابتدا متد resolveCurrentTenantIdentifier را فراخوانی میکند تا مستاجر را شناسایی کند و اگر بخواهیم تمامی مستاجر ها Validate شوند از متد validateExistingCurrentSessions با مقدار برگشتی true استفاده میکنیم
رویکرد Schema :
در این استراتژی ما از Schema یا نام کاربری متفاوت در یک دیتابیس فیزیکی استفاده میکنیم این رویکرد در شرایطی استفاده میشود که نیاز به بیشترین پرفرمانس داشته باشیم و بتوانیم از ویژگی ایجاد Backup دیتابیس به ازای هر مستاجر استفاده کنیم
public abstract class MultitenancyIntegrationTest { @Mock private CurrentTenantIdentifierResolver currentTenantIdentifierResolver; private SessionFactory sessionFactory; @Before public void setup() throws IOException { MockitoAnnotations.initMocks(this); when(currentTenantIdentifierResolver.validateExistingCurrentSessions()) .thenReturn(false); Properties properties = getHibernateProperties(); properties.put( AvailableSettings.MULTI_TENANT_IDENTIFIER_RESOLVER, currentTenantIdentifierResolver); sessionFactory = buildSessionFactory(properties); initTenant(TenantIdNames.MYDB1); initTenant(TenantIdNames.MYDB2); } protected void initTenant(String tenantId) { when(currentTenantIdentifierResolver .resolveCurrentTenantIdentifier()) .thenReturn(tenantId); createCarTable(); } }
پیاده سازی MultiTenantConnectionProvider :
در این پیاده سازی هر زمانی که Connection نیاز بود ایجاد شود اسکیمای مورد نظر را ست میکند
class SchemaMultiTenantConnectionProvider extends AbstractMultiTenantConnectionProvider { private ConnectionProvider connectionProvider; public SchemaMultiTenantConnectionProvider() throws IOException { this.connectionProvider = initConnectionProvider(); } @Override protected ConnectionProvider getAnyConnectionProvider() { return connectionProvider; } @Override protected ConnectionProvider selectConnectionProvider( String tenantIdentifier) { return connectionProvider; } @Override public Connection getConnection(String tenantIdentifier) throws SQLException { Connection connection = super.getConnection(tenantIdentifier); connection.createStatement() .execute(String.format("SET SCHEMA %s;", tenantIdentifier)); return connection; } private ConnectionProvider initConnectionProvider() throws IOException { Properties properties = new Properties(); properties.load(getClass() .getResourceAsStream("/hibernate.properties")); DriverManagerConnectionProviderImpl connectionProvider = new DriverManagerConnectionProviderImpl(); connectionProvider.configure(properties); return connectionProvider; } }
در اینجا دو مستاجر داریم که برای هر یک اسکیمای جداگانه ای در نظر گرفته شده
تنظیمات hibernate.properties برای استفاده از schema در حالت چند مستاجری و اینترفیس MultiTenantConnectionProvider :
hibernate.connection.url=jdbc:h2:mem:mydb1;DB_CLOSE_DELAY=-1;\ INIT=CREATE SCHEMA IF NOT EXISTS MYDB1\\;CREATE SCHEMA IF NOT EXISTS MYDB2\\; hibernate.multiTenancy=SCHEMA hibernate.multi_tenant_connection_provider=\ com.baeldung.hibernate.multitenancy.schema.SchemaMultiTenantConnectionProvider
ما برای شرایط تست در hibernate.connection.url دو اسکیما را کانفیگ کردیم که در برنامه واقعی لازم نیست و باید اسکیما ها از قبل آماده استفاده باشند
تست :
یک داده از نوع Car را برای مستاجر اول مربوط به اسکیمای myDb1 ذخیره کردیم و سپس آنرا از دیتابیس فراخوانی کردیم و بعد آنرا از myDb2 که مربوط به مستاجر دوم است فراخوانی میکنیم ولی دیتایی موجود نیست پس هر مستاجر دیتای مربوط به خود را بصورت ایزوله خواهد داشت :
@Test void whenAddingEntries_thenOnlyAddedToConcreteDatabase() { whenCurrentTenantIs(TenantIdNames.MYDB1); whenAddCar("myCar"); thenCarFound("myCar"); whenCurrentTenantIs(TenantIdNames.MYDB2); thenCarNotFound("myCar"); }
رویکرد Database :
رویکرد چند مستاجری Database برای فراهم کردن دیتابیس فیزیکی مجزا به ازای هر مستاجر است و از آنجا که دیتابیس هر مستاجر کاملا ایزوله است باید زمانی این استراتژی انتخاب شود که ویژگی های خاصی نظیر Backup گیری دیتابیس به ازای هر مستاجر مورد نیاز باشد
برای مثال، مانند قبل از کلاس MultitenancyIntegrationTest و اینترفیس CurrentTenantIdentifierResolver استفاده میکنیم و برای اینترفیس MultiTenantConnectionProvider ما از یک Map استفاده کردیم که ConnectionProvider های شناسایی شده هر مستاجر در آن نگهداری میشود :
class MapMultiTenantConnectionProvider extends AbstractMultiTenantConnectionProvider { private Map<String, ConnectionProvider> connectionProviderMap = new HashMap<>(); public MapMultiTenantConnectionProvider() throws IOException { initConnectionProviderForTenant(TenantIdNames.MYDB1); initConnectionProviderForTenant(TenantIdNames.MYDB2); } @Override protected ConnectionProvider getAnyConnectionProvider() { return connectionProviderMap.values() .iterator() .next(); } @Override protected ConnectionProvider selectConnectionProvider( String tenantIdentifier) { return connectionProviderMap.get(tenantIdentifier); } private void initConnectionProviderForTenant(String tenantId) throws IOException { Properties properties = new Properties(); properties.load(getClass().getResourceAsStream( String.format("/hibernate-database-%s.properties", tenantId))); DriverManagerConnectionProviderImpl connectionProvider = new DriverManagerConnectionProviderImpl(); connectionProvider.configure(properties); this.connectionProviderMap.put(tenantId, connectionProvider); } }
ست کردن تنظیمات دیتابیس به ازای هر مستاجر :
هر ConnectionProvider مربوط به هر مستاجر از طریق فایل جداگانه با الگوی hibernate-database-<tenant identifier>.properties کانفیگ میشود
hibernate.connection.driver_class=org.h2.Driver hibernate.connection.url=jdbc:h2:mem:<Tenant Identifier>;DB_CLOSE_DELAY=-1 hibernate.connection.username=sa hibernate.dialect=org.hibernate.dialect.H2Dialect
و در hibernate.properties حالت چند مستاجری database را فعال میکنیم و پیاده ساز اینترفیس MultiTenantConnectionProvider را نیز معرفی میکنیم :
hibernate.multiTenancy=DATABASE hibernate.multi_tenant_connection_provider=\ com.baeldung.hibernate.multitenancy.database.MapMultiTenantConnectionProvider
تست آن مانند حالت قبل قابل انجام است