
??http://mp.baomidou.com/guide/tenant.html??
一般的程序應用當使用者訪問不同,并且進入相對應的程序頁面,則會把用戶相關數據傳輸到后臺這里。在傳輸的時候需要帶上標識(租戶ID),以便程序將數據進行隔離。當不同的租戶使用同一個程序服務,這里就需要考慮一個數據隔離的情況。
?
(資料圖)
按照綜合實際考慮,一般都會采用方案三,即共享數據庫,共享數據架構,因為這種方案服務器成本最低,但是提高了開發(fā)成本。
第一步:應用添加維護一張tenant(租戶表),需要進行隔離的數據表上新增租戶id,例如,現在有數據庫表(user)如下:Mybatis-plus就提供了一種多租戶的解決方案,實現方式是基于分頁插件(攔截器)進行實現的;
字段名 | 字段類型 | 描述 |
id | Long | 主鍵 |
tenantId | Long | 租戶編碼 |
other | varchar(256) | 其他屬性 |
將tenantId視為租戶ID,用來隔離租戶與租戶之間的數據,如果要查詢當前服務商的用戶,SQL大致如下:
SELECT * FROM table t WHERE t.tenantId = 1;第二步:實現TenantHandler接口并實現它的方法:
public interface TenantHandler { /** * 獲取租戶 ID 值表達式,支持多個 ID 條件查詢 * 支持自定義表達式,比如:tenant_id in (1,2) @since 2019-8-2 * @param where 參數 true 表示為 where 條件 false 表示為 insert 或者 select 條件 * @return 租戶 ID 值表達式 */ Expression getTenantId(boolean where); /** * 獲取租戶字段名 * @return 租戶字段名 */ String getTenantIdColumn(); /** * 根據表名判斷是否進行過濾 * @param tableName 表名 * @return 是否進行過濾, true:表示忽略,false:需要解析多租戶字段 */ boolean doTableFilter(String tableName);}
@Slf4j@Componentpublic class PreTenantHandler implements TenantHandler { @Autowired private PreTenantConfigProperties configProperties; /** * 租戶Id * @return */ @Override public Expression getTenantId(boolean where) { //可以通過過濾器從請求中獲取對應租戶id Long tenantId = PreTenantContextHolder.getCurrentTenantId(); log.debug("當前租戶為{}", tenantId); if (tenantId == null) { return new NullValue(); } return new LongValue(tenantId); } /** * 租戶字段名 * @return */ @Override public String getTenantIdColumn() { return configProperties.getTenantIdColumn(); } /** * 根據表名判斷是否進行過濾 * 忽略掉一些表:如租戶表(sys_tenant)本身不需要執(zhí)行這樣的處理 * @param tableName * @return */ @Override public boolean doTableFilter(String tableName) { return configProperties.getIgnoreTenantTables().stream().anyMatch((e) -> e.equalsIgnoreCase(tableName)); }}第三步:配置mybatisPlus的分頁插件配置
租戶相關的表,我們都需要不厭其煩的加上AND t.tenantId = ?查詢條件,稍不注意就會導致數據越界,數據安全問題讓人擔憂。好在有了MybatisPlus這個神器,可以極為方便的實現多租戶SQL解析器。
@Configuration@MapperScan("com.wuwenze.mybatisplusmultitenancy.mapper")public class MybatisPlusConfig { private static final String SYSTEM_TENANT_ID = "provider_id"; private static final ListIGNORE_TENANT_TABLES = Lists.newArrayList("provider"); @Autowired private ApiContext apiContext; @Bean public PaginationInterceptor paginationInterceptor() { PaginationInterceptor paginationInterceptor = new PaginationInterceptor(); // SQL解析處理攔截:增加租戶處理回調。 TenantSqlParser tenantSqlParser = new TenantSqlParser() .setTenantHandler(new TenantHandler() { @Override public Expression getTenantId() { // 從當前系統上下文中取出當前請求的服務商ID,通過解析器注入到SQL中。 Long currentProviderId = apiContext.getCurrentProviderId(); if (null == currentProviderId) { throw new RuntimeException("#1129 getCurrentProviderId error."); } return new LongValue(currentProviderId); } @Override public String getTenantIdColumn() { return SYSTEM_TENANT_ID; } @Override public boolean doTableFilter(String tableName) { // 忽略掉一些表:如租戶表(provider)本身不需要執(zhí)行這樣的處理。 return IGNORE_TENANT_TABLES.stream().anyMatch((e) -> e.equalsIgnoreCase(tableName)); } }); paginationInterceptor.setSqlParserList(Lists.newArrayList(tenantSqlParser)); return paginationInterceptor; } @Bean(name = "performanceInterceptor") public PerformanceInterceptor performanceInterceptor() { return new PerformanceInterceptor(); }}
配置好之后,不管是查詢、新增、修改刪除方法,MP都會自動加上租戶ID的標識,測試如下:
@Testpublic void select(){ Listusers = userMapper.selectList(Wrappers. lambdaQuery().eq(User::getAge, 18)); users.forEach(System.out::println);}
DEBUG==> Preparing: SELECT id, login_name, name, password, email, salt, sex, age, phone, user_type, status, organization_id, create_time, update_time, version, tenant_id FROM sys_user WHERE sys_user.tenant_id = "001" AND is_delete = "0" AND age = ?
注:特定SQL過濾,如果在程序中,有部分SQL不需要加上租戶ID的表示,需要過濾特定的sql,可以通過如下兩種方式:
在配置分頁插件中加上配置ISqlParserFilter解析器,配置SQL很多,比較麻煩,不建議;
paginationInterceptor.setSqlParserFilter(new ISqlParserFilter() { @Override public boolean doFilter(MetaObject metaObject) { MappedStatement ms = SqlParserHelper.getMappedStatement(metaObject); // 對應Mapper、dao中的方法 if("com.example.demo.mapper.UserMapper.selectList".equals(ms.getId())){ return true; } return false; }});
通過租戶注解 @SqlParser(filter = true) 的形式,目前只能作用于Mapper的方法上:
public interface UserMapper extends BaseMapper{ /** * 自定Wrapper修改 * * @param userWrapper 條件構造器 * @param user 修改的對象參數 * @return */ @SqlParser(filter = true) int updateByMyWrapper(@Param(Constants.WRAPPER) Wrapper userWrapper, @Param("user") User user);}
@Componentpublic class ApiContext { private static final String KEY_CURRENT_PROVIDER_ID = "KEY_CURRENT_PROVIDER_ID"; private static final MapmContext = Maps.newConcurrentMap(); public void setCurrentProviderId(Long providerId) { mContext.put(KEY_CURRENT_PROVIDER_ID, providerId); } public Long getCurrentProviderId() { return (Long) mContext.get(KEY_CURRENT_PROVIDER_ID); }}