MyBatis-Plus之多租戶架構(Multi-tenancy)——SAAS
作者:梅西愛騎車
連結:https://www.jianshu.com/p/742f40eb9937
來源:簡書
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。
一、什麼是多租戶
多租戶技術或稱多重租賃技術,簡稱多租戶。是一種軟體架構技術,是實現如何在多使用者環境下(此處的多使用者一般是面向企業使用者)共用相同的系統或程式元件,並且可確保各使用者間資料的隔離性。
簡單講:在一臺伺服器上執行單個應用例項,它為多個租戶(客戶)提供服務。從定義中我們可以理解:多租戶是一種架構,目的是為了讓多使用者環境下使用同一套程式,且保證使用者間資料隔離。那麼重點就很淺顯易懂了,多租戶的重點就是同一套程式下實現多使用者資料的隔離。SaaS
二、資料隔離有三種方案
- 獨立資料庫:簡單來說就是一個租戶使用一個數據庫,這種資料隔離級別最高,安全性最好,但是提高成本。
- 共享資料庫、隔離資料架構:多租戶使用同一個資料庫,但是每個租戶對應一個Schema(資料庫user)。
- 共享資料庫、共享資料架構:使用同一個資料庫,同一個Schema,但是在表中增加了
租戶ID
的欄位,這種共享資料程度最高,隔離級別最低。
這裡採用方案三,即共享資料庫,共享資料架構,因為這種方案伺服器成本最低,但是提高了開發成本。
三、Mybatis-plus實現多租戶方案
為什麼選擇MyBatisPlus?
除了一些系統共用的表以外,其他租戶相關的表,我們都需要在sql不厭其煩的加上AND t.tenant_id = ?
查詢條件,稍不注意就會導致資料越界,資料安全問題讓人擔憂。好在有了MybatisPlus這個神器,可以極為方便的實現多租戶SQL解析器。
Mybatis-plus就提供了一種多租戶的解決方案,實現方式是基於分頁外掛(攔截器)進行實現的。
3.1 第一步:
在應用新增維護一張sys_tenant(租戶管理表),在需要進行隔離的資料表上新增租戶id;
3.2 第二步:
建立表:
CREATE TABLE `orders_1`.`tenant` ( `id` int(0) NOT NULL AUTO_INCREMENT COMMENT '自增主鍵', `expire_date`datetime(0) COMMENT '協議到期時間', `amount` decimal(8, 2) COMMENT '金額', `tenant_id` int(0) COMMENT '租戶ID', PRIMARY KEY (`id`) );
自定義系統的上下文,儲存從cookie等方式獲取的租戶ID,在後續的getTenantId()使用。
package com.erbadagang.mybatis.plus.tenant.config; import org.springframework.stereotype.Component; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * @description 系統的上下文幫助類。ConcurrentHashMap設定租戶ID,供後續的MP的getTenantId()取出 * @ClassName: ApiContext * @author: 郭秀志 [email protected] * @date: 2020/7/12 21:50 * @Copyright: */ @Component public class ApiContext { private static final String KEY_CURRENT_TENANT_ID = "KEY_CURRENT_TENANT_ID"; private static final Map<String, Object> mContext = new ConcurrentHashMap<>(); public void setCurrentTenantId(Long providerId) { mContext.put(KEY_CURRENT_TENANT_ID, providerId); } public Long getCurrentTenantId() { return (Long) mContext.get(KEY_CURRENT_TENANT_ID); } }
核心類——MyBatisPlusConfig
通過分頁外掛配置MP多租戶。
package com.erbadagang.mybatis.plus.tenant.config; import com.baomidou.mybatisplus.core.parser.ISqlParser; import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor; import com.baomidou.mybatisplus.extension.plugins.tenant.TenantHandler; import com.baomidou.mybatisplus.extension.plugins.tenant.TenantSqlParser; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.LongValue; import org.mybatis.spring.annotation.MapperScan; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.ArrayList; import java.util.List; /** * @description MyBatisPlus配置類,分頁外掛,多租戶也是使用的分頁外掛進行的配置。 * @ClassName: MyBatisPlusConfig * @author: 郭秀志 [email protected] * @date: 2020/7/12 21:34 * @Copyright: */ @Configuration @MapperScan("com.erbadagang.mybatis.plus.tenant.mapper")//配置掃描的mapper包 public class MyBatisPlusConfig { @Autowired private ApiContext apiContext; /** * 分頁外掛 * * @return */ @Bean public PaginationInterceptor paginationInterceptor() { PaginationInterceptor paginationInterceptor = new PaginationInterceptor(); // 建立SQL解析器集合 List<ISqlParser> sqlParserList = new ArrayList<>(); // 建立租戶SQL解析器 TenantSqlParser tenantSqlParser = new TenantSqlParser(); // 設定租戶處理器 tenantSqlParser.setTenantHandler(new TenantHandler() { // 設定當前租戶ID,實際情況你可以從cookie、或者快取中拿都行 @Override public Expression getTenantId(boolean select) { // 從當前系統上下文中取出當前請求的服務商ID,通過解析器注入到SQL中。 Long currentProviderId = apiContext.getCurrentTenantId(); if (null == currentProviderId) { throw new RuntimeException("Get CurrentProviderId error."); } return new LongValue(currentProviderId); } @Override public String getTenantIdColumn() { // 對應資料庫中租戶ID的列名 return "tenant_id"; } @Override public boolean doTableFilter(String tableName) { // 是否需要需要過濾某一張表 /* List<String> tableNameList = Arrays.asList("sys_user"); if (tableNameList.contains(tableName)){ return true; }*/ return false; } }); sqlParserList.add(tenantSqlParser); paginationInterceptor.setSqlParserList(sqlParserList); return paginationInterceptor; } }
四、測試
配置好之後,不管是查詢、新增、修改刪除方法,MP都會自動加上租戶ID的標識,測試如下:
package com.erbadagang.mybatis.plus.tenant; import com.erbadagang.mybatis.plus.tenant.config.ApiContext; import com.erbadagang.mybatis.plus.tenant.entity.Tenant; import com.erbadagang.mybatis.plus.tenant.mapper.TenantMapper; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.util.List; /** * @description 多租戶測試用例 * @ClassName: MultiTanentApplicationTests * @author: 郭秀志 [email protected] * @date: 2020/7/12 22:06 * @Copyright: */ @SpringBootTest class MultiTanentApplicationTests { @Autowired private ApiContext apiContext; @Autowired private TenantMapper tenantMapper; @Test public void before() { // 在上下文中設定當前服務商的ID apiContext.setCurrentTenantId(1L); } @Test public void select() { List<Tenant> tenants = tenantMapper.selectList(null); tenants.forEach(System.out::println); } }
輸出的SQL自動包括WHERE tenant_id = 1
:
==> Preparing: SELECT id, expire_date, amount, tenant_id FROM t_tenant WHERE tenant_id = 1
==> Parameters:
<== Total: 0
五、特定SQL過濾
如果在程式中,有部分SQL不需要加上租戶ID的表示,需要過濾特定的sql,可以通過如下兩種方式:
5.1 方式一:
在配置分頁外掛中加上配置ISqlParserFilter解析器,如果配置SQL很多,比較麻煩,不建議。
//有部分SQL不需要加上租戶ID的表示,需要過濾特定的sql。如果比較多不建議這裡配置。 /*paginationInterceptor.setSqlParserFilter(new ISqlParserFilter() { @Override public boolean doFilter(MetaObject metaObject) { MappedStatement ms = SqlParserHelper.getMappedStatement(metaObject); // 對應Mapper或者dao中的方法 if("com.erbadagang.mybatis.plus.tenant.mapper.UserMapper.selectList".equals(ms.getId())){ return true; } return false; } });*/
5.2 方式二:
通過租戶註解的形式,目前只能作用於Mapper的方法上。特定sql過濾 過濾特定的方法 也可以在userMapper需要排除的方法上加入註解SqlParser(filter=true) 排除 SQL 解析。
package com.erbadagang.mybatis.plus.tenant.mapper; import com.baomidou.mybatisplus.annotation.SqlParser; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.erbadagang.mybatis.plus.tenant.entity.Tenant; import org.apache.ibatis.annotations.Select; /** * <p> * Mapper 介面 * </p> * * @author 郭秀志 [email protected] * @since 2020-07-12 */ public interface TenantMapper extends BaseMapper<Tenant> { /** * 自定Wrapper, @SqlParser(filter = true)註解代表不進行SQL解析也就沒有租戶的附加條件。 * * @return */ @SqlParser(filter = true) @Select("SELECT count(5) FROM t_tenant ") public Integer myCount(); }
測試
@Test public void myCount() { Integer count = tenantMapper.myCount(); System.out.println(count); }
SQL輸出
==> Preparing: SELECT count(5) FROM t_tenant ==> Parameters: <== Columns: count(5) <== Row: 0 <== Total: 1
開啟 SQL 解析快取註解生效,如果你的MP版本在3.1.1及以上則不需要配置
# 開啟 SQL 解析快取註解生效,如果你的MP版本在3.1.1及以上則不需要配置 mybatis-plus: global-config: sql-parser-cache: true
本文原始碼使用 Apache License 2.0開源許可協議,可從Gitee程式碼地址通過git clone
命令下載到本地或者通過瀏覽器方式檢視原始碼。