Fluent Mybatis實(shí)現(xiàn)環(huán)境隔離和租戶(hù)隔離
什么是環(huán)境隔離和多租戶(hù)隔離
我們?cè)趯?shí)際的業(yè)務(wù)開(kāi)發(fā)中,經(jīng)常會(huì)碰到環(huán)境邏輯隔離和租戶(hù)數(shù)據(jù)邏輯隔離的問(wèn)題。
環(huán)境隔離
我們的開(kāi)發(fā)系統(tǒng)過(guò)程中,經(jīng)常會(huì)涉及到日常開(kāi)發(fā)環(huán)境,測(cè)試環(huán)境,預(yù)發(fā)環(huán)境和線(xiàn)上環(huán)境,如何區(qū)隔這些環(huán)境,有些方案是采用獨(dú)立的數(shù)據(jù)庫(kù),有些是采用同一套數(shù)據(jù)庫(kù)(比如線(xiàn)下多個(gè)測(cè)試環(huán)境使用同一個(gè)數(shù)據(jù)庫(kù),預(yù)發(fā)環(huán)境和線(xiàn)上環(huán)境使用同一個(gè)數(shù)據(jù)庫(kù)),然后對(duì)數(shù)據(jù)進(jìn)行打標(biāo)的辦法,來(lái)區(qū)分不同環(huán)境的數(shù)據(jù)。
多租戶(hù)管理
在復(fù)雜的業(yè)務(wù)系統(tǒng)中,比如SaaS應(yīng)用中,在多用戶(hù)環(huán)境下共用相同的系統(tǒng)或程序組件,如何確保各用戶(hù)間數(shù)據(jù)的隔離性。簡(jiǎn)單講:在一臺(tái)服務(wù)器上運(yùn)行單個(gè)應(yīng)用實(shí)例,它為多個(gè)租戶(hù)(客戶(hù))提供服務(wù)。從定義中我們可以理解:多租戶(hù)是一種架構(gòu),是為了讓多用戶(hù)環(huán)境下使用同一套程序,但要保證用戶(hù)間數(shù)據(jù)隔離。那如何進(jìn)行多租戶(hù)的重點(diǎn)就是同一套程序下實(shí)現(xiàn)多用戶(hù)數(shù)據(jù)的隔離,做法其實(shí)和環(huán)境隔離是同一個(gè)道理。
這里采用多環(huán)境多租戶(hù)共用數(shù)據(jù)表的場(chǎng)景,來(lái)探討下FluentMybatis是如何支持多環(huán)境和多租戶(hù)管理的。
環(huán)境隔離和多租戶(hù)隔離需要做的事情
比如我們有下面表
create table student
(
id bigint(21) unsigned auto_increment comment '主鍵id'
primary key,
age int null comment '年齡',
grade int null comment '年級(jí)',
user_name varchar(45) null comment '名字',
gender_man tinyint(2) default 0 null comment '性別, 0:女; 1:男',
birthday datetime null comment '生日',
phone varchar(20) null comment '電話(huà)',
bonus_points bigint(21) default 0 null comment '積分',
status varchar(32) null comment '狀態(tài)(字典)',
home_county_id bigint(21) null comment '家庭所在區(qū)縣',
home_address_id bigint(21) null comment 'home_address外鍵',
address varchar(200) null comment '家庭詳細(xì)住址',
version varchar(200) null comment '版本號(hào)',
env varchar(10) NULL comment '數(shù)據(jù)隔離環(huán)境',
tenant bigint NOT NULL default 0 comment '租戶(hù)標(biāo)識(shí)',
gmt_created datetime null comment '創(chuàng)建時(shí)間',
gmt_modified datetime null comment '更新時(shí)間',
is_deleted tinyint(2) default 0 null comment '是否邏輯刪除'
) ENGINE = InnoDB
DEFAULT CHARSET = utf8
COMMENT '學(xué)生信息表';
注意其中的2個(gè)字段
- env, 表示應(yīng)用部署的環(huán)境, 環(huán)境的區(qū)隔一般是采用應(yīng)用部署的機(jī)器環(huán)境變量。
- tenant, 表示數(shù)據(jù)所屬租戶(hù),租戶(hù)的隔離一般是通過(guò)登錄用戶(hù)信息獲取的。
對(duì)環(huán)境和租戶(hù)的隔離,主要是CRUD過(guò)程中,需要帶上環(huán)境變量和租戶(hù)信息。如果沒(méi)有框架的支持,就需要在構(gòu)造SQL的過(guò)程中,手動(dòng)設(shè)置env和tenant。這就存在一個(gè)嚴(yán)重的弊端: 在編碼過(guò)程中,需要時(shí)刻注意sql語(yǔ)句中不要漏了這2個(gè)條件,否則就會(huì)產(chǎn)生邏輯錯(cuò)誤和信息泄露。
為了減少錯(cuò)誤,我們都會(huì)將邏輯進(jìn)行收攏,下面我們演示fluent mybatis如何統(tǒng)一處理。
環(huán)境隔離和租戶(hù)隔離工具類(lèi)
為了進(jìn)行環(huán)境隔離和租戶(hù)隔離,我們一般會(huì)統(tǒng)一定義獲取環(huán)境變量和租戶(hù)信息的工具類(lèi)。
環(huán)境隔離工具類(lèi)
/**
* 應(yīng)用部署環(huán)境工具類(lèi)
*/
public class EnvUtils {
public static String currEnv() {
// 應(yīng)用啟動(dòng)時(shí), 讀取的機(jī)器部署環(huán)境變量, 這里簡(jiǎn)化為返回固定值演示
return "test1";
}
}
租戶(hù)隔離工具類(lèi)
/**
* 獲取用戶(hù)所屬租戶(hù)信息工具類(lèi)
*/
public class TenantUtils {
/**
* 租戶(hù)A
*/
static final long A_TENANT = 111111L;
/**
* 租戶(hù)B
*/
static final long B_TENANT = 222222L;
/**
* 租戶(hù)信息一般根據(jù)登錄用戶(hù)身份來(lái)判斷, 這里簡(jiǎn)化為偶數(shù)用戶(hù)屬于租戶(hù)A, 奇數(shù)用戶(hù)屬于租戶(hù)B
*
* @return
*/
public static long findUserTenant() {
long userId = loginUserId();
if (userId % 2 == 0) {
return A_TENANT;
} else {
return B_TENANT;
}
}
/**
* 當(dāng)前登錄的用戶(hù)id, 一般從Session中獲取
*
* @return
*/
public static long loginUserId() {
return 1L;
}
}
隔離前準(zhǔn)備工作
Entity隔離屬性基類(lèi)
為了方便對(duì)所有需要隔離的Entity進(jìn)行統(tǒng)一的環(huán)境和租戶(hù)信息的設(shè)置和讀取,我們把Entity的環(huán)境和租戶(hù)的屬性的getter和setter方法定義到一個(gè)接口上。
/**
* Entity類(lèi)隔離屬性基類(lèi)
*/
public interface IsolateEntity {
/**
* 返回entity env屬性值
*
* @return
*/
String getEnv();
/**
* 設(shè)置entity env屬性值
*
* @param env
* @return
*/
IsolateEntity setEnv(String env);
/**
* 返回entity 租戶(hù)信息
*
* @return
*/
Long getTenant();
/**
* 設(shè)置entity 租戶(hù)信息
*
* @param tenant
* @return
*/
IsolateEntity setTenant(Long tenant);
}
這樣所有需要隔離的Entity只要繼承這個(gè)接口就可以在需要隔離操作的地方把具體的entity當(dāng)作IsolateEntity對(duì)象來(lái)操作。
隔離屬性和默認(rèn)條件設(shè)置
有了統(tǒng)一的接口,我們還需要一個(gè)默認(rèn)進(jìn)行設(shè)置的操作,fluent mybatis提供了一個(gè)IDefaultSetter 接口,可以對(duì)Entity,Query和Update進(jìn)行攔截操作。
/**
* 增刪改查中,環(huán)境和租戶(hù)隔離設(shè)置
*/
public interface IsolateSetter extends IDefaultSetter {
/**
* 插入的entity,如果沒(méi)有顯式設(shè)置環(huán)境和租戶(hù),根據(jù)工具類(lèi)進(jìn)行默認(rèn)設(shè)置
*
* @param entity
*/
@Override
default void setInsertDefault(IEntity entity) {
IsolateEntity isolateEntity = (IsolateEntity) entity;
if (isolateEntity.getEnv() == null) {
isolateEntity.setEnv(EnvUtils.currEnv());
}
if (isolateEntity.getTenant() == null) {
isolateEntity.setTenant(TenantUtils.findUserTenant());
}
}
/**
* 查詢(xún)條件追加環(huán)境隔離和租戶(hù)隔離
*
* @param query
*/
@Override
default void setQueryDefault(IQuery query) {
query.where()
.apply("env", SqlOp.EQ, EnvUtils.currEnv())
.apply("tenant", SqlOp.EQ, TenantUtils.findUserTenant());
}
/**
* 更新條件追加環(huán)境隔離和租戶(hù)隔離
*
* @param updater
*/
@Override
default void setUpdateDefault(IUpdate updater) {
updater.where()
.apply("env", SqlOp.EQ, EnvUtils.currEnv())
.apply("tenant", SqlOp.EQ, TenantUtils.findUserTenant());
}
}
為了避免使用不當(dāng)導(dǎo)致線(xiàn)程安全問(wèn)題(變量共享), fluent mybatis只允許在應(yīng)用中定義接口(比如這里的IsolateSetter)繼承IDefaultSetter, 不允許定義成類(lèi)。
代碼生成設(shè)置
怎么讓fluent mybatis識(shí)別到哪些Entity可以繼承IsolateEntity,哪些Entity操作需要進(jìn)行IsolateSetter統(tǒng)一攔截呢?
在@FluentMybatis上有個(gè)屬性defaults(), 我們把defaults值設(shè)置為 IsolateSetter.class就可以了。
public @interface FluentMybatis {
/**
* entity, query, updater默認(rèn)值設(shè)置實(shí)現(xiàn)
*
* @return
*/
Class<? extends IDefaultSetter> defaults() default IDefaultSetter.class;
}
當(dāng)然,我們并不需要手動(dòng)去修改Entity類(lèi),只需要在代碼生成上設(shè)置。
public class FluentGenerateMain {
static final String url = "jdbc:mysql://localhost:3306/fluent_mybatis?useSSL=false&useUnicode=true&characterEncoding=utf-8";
/**
* 生成代碼的package路徑
*/
static final String basePackage = "cn.org.fluent.mybatis.many2many.demo";
public static void main(String[] args) {
FileGenerator.build(Noting.class);
}
@Tables(
/** 數(shù)據(jù)庫(kù)連接信息 **/
url = url, username = "root", password = "password",
/** Entity類(lèi)parent package路徑 **/
basePack = basePackage,
/** Entity代碼源目錄 **/
srcDir = "example/many2many_demo/src/main/java",
/** 如果表定義記錄創(chuàng)建,記錄修改,邏輯刪除字段 **/
gmtCreated = "gmt_created", gmtModified = "gmt_modified", logicDeleted = "is_deleted",
/** 需要生成文件的表 ( 表名稱(chēng):對(duì)應(yīng)的Entity名稱(chēng) ) **/
tables = @Table(value = {"student"},
entity = IsolateEntity.class,
defaults = IsolateSetter.class)
)
static class Noting {
}
}
注意,對(duì)比之前的代碼生成,@Table上多了2個(gè)屬性設(shè)置
// 標(biāo)識(shí)對(duì)應(yīng)的Entity類(lèi)需要繼承的接口 entity = IsolateEntity.class
// 標(biāo)識(shí)對(duì)應(yīng)的Entity類(lèi)CRUD過(guò)程中需要進(jìn)行的默認(rèn)設(shè)置操作 defaults = IsolateSetter.class
執(zhí)行代碼生成,Entity代碼如下:
@FluentMybatis(
table = "student",
defaults = IsolateSetter.class
)
public class StudentEntity extends RichEntity implements IsolateEntity {
// ... 省略
}
我們看到@FluentMybatis設(shè)置了defaults屬性,Entity類(lèi)繼承了IsolateEntity接口。
接下來(lái),我們進(jìn)行具體的增刪改查演示。
增刪改查環(huán)境和租戶(hù)隔離演示
新增數(shù)據(jù)
@RunWith(SpringRunner.class)
@SpringBootTest(classes = AppMain.class)
public class InsertWithEnvDemo {
@Autowired
private StudentMapper mapper;
@Test
public void insertEntity() {
mapper.delete(new StudentQuery());
mapper.insert(new StudentEntity()
.setAddress("宇宙深處")
.setUserName("FluentMybatis")
);
StudentEntity student = mapper.findOne(StudentQuery.query()
.where.userName().eq("FluentMybatis").end()
.limit(1));
System.out.println(student.getUserName() + ", env:" + student.getEnv() + ", tenant:" + student.getTenant());
}
}
查看控制臺(tái)輸出log
DEBUG - ==> Preparing:
INSERT INTO student(gmt_created, gmt_modified, is_deleted, address, env, tenant, user_name)
VALUES (now(), now(), 0, ?, ?, ?, ?)
DEBUG - ==> Parameters: 宇宙深處(String), test1(String), 222222(Long), FluentMybatis(String)
DEBUG - <== Updates: 1
DEBUG - ==> Preparing: SELECT id, gmt_created, gmt_modified, is_deleted, address, age, birthday, bonus_points, env, gender_man, grade, home_address_id, home_county_id, phone, status, tenant, user_name, version
FROM student WHERE user_name = ? LIMIT ?, ?
DEBUG - ==> Parameters: FluentMybatis(String), 0(Integer), 1(Integer)
DEBUG - <== Total: 1
FluentMybatis, env:test1, tenant:222222
在演示例子中,我們雖然只顯式設(shè)置了userName和address2個(gè)屬性,但插入數(shù)據(jù)中設(shè)置了7個(gè)屬性,其中包括env和tenant。
注意,這里的查詢(xún)條件并沒(méi)有帶上環(huán)境變量
查詢(xún)數(shù)據(jù)
fluent mybatis提供了2種構(gòu)造查詢(xún)器的方式
- XyzQuery.query(): 全新的不帶任何條件的查詢(xún)。
- XyzQuery.defaultQuery(): 按照@FluentMybatis defaults屬性指定的接口,設(shè)置好默認(rèn)查詢(xún)條件。
上面默認(rèn)插入的例子已經(jīng)演示了不帶條件的query()查詢(xún),我們現(xiàn)在演示下設(shè)置了默認(rèn)條件的查詢(xún)。
@RunWith(SpringRunner.class)
@SpringBootTest(classes = AppMain.class)
public class QueryWithEnvDemo {
@Autowired
private StudentMapper mapper;
@Test
public void testQueryWithEnv(){
mapper.delete(new StudentQuery());
mapper.insert(new StudentEntity()
.setAddress("宇宙深處")
.setUserName("FluentMybatis")
);
StudentEntity student = mapper.findOne(mapper.defaultQuery()
.where.userName().eq("FluentMybatis").end()
.limit(1));
System.out.println(student.getUserName() + ", env:" + student.getEnv() + ", tenant:" + student.getTenant());
}
}
查看控制log輸出
DEBUG - ==> Preparing: SELECT id, gmt_created, ... , tenant, user_name, version
FROM student
WHERE env = ?
AND tenant = ?
AND user_name = ?
LIMIT ?, ?
DEBUG - ==> Parameters: test1(String), 222222(Long), FluentMybatis(String), 0(Integer), 1(Integer)
DEBUG - <== Total: 1
FluentMybatis, env:test1, tenant:222222
我們看到,查詢(xún)條件中除了有我們?cè)O(shè)置好的user_name,還包括在IsolateSetter接口中設(shè)置好的env和tenant字段。
更新數(shù)據(jù)
和Query一樣,Updater同樣提供了2個(gè)方法來(lái)構(gòu)造Updater
- XyzUpdate.updater() : 不帶任何條件的更新。
- XyzUpdate.defaultUpdater(): 根據(jù)IsolateSetter#setUpdateDefault方法設(shè)置好更新條件。
演示例子
@RunWith(SpringRunner.class)
@SpringBootTest(classes = AppMain.class)
public class UpdateWithEnvDemo {
@Autowired
private StudentMapper mapper;
@Test
public void testQueryWithEnv() {
mapper.delete(new StudentQuery());
mapper.insert(new StudentEntity()
.setAddress("宇宙深處")
.setUserName("FluentMybatis")
);
mapper.updateBy(StudentUpdate.defaultUpdater()
.update.address().is("回到地球").end()
.where.userName().eq("FluentMybatis").end()
);
}
}
查看控制臺(tái)log輸出
DEBUG - ==> Preparing: UPDATE student
SET gmt_modified = now(), address = ?
WHERE env = ?
AND tenant = ?
AND user_name = ?
DEBUG - ==> Parameters: 回到地球(String), test1(String), 222222(Long), FluentMybatis(String)
DEBUG - <== Updates: 1
更新條件中自動(dòng)帶上了設(shè)置好的默認(rèn)條件 env 和 tenant。
總結(jié)
Fluent Mybatis通過(guò)自定義接口繼承IDefaultSetter,賦予了你進(jìn)行數(shù)據(jù)隔離操作的強(qiáng)大功能。默認(rèn)值的賦值是通過(guò)編譯生成的XyzDefaults類(lèi)來(lái)進(jìn)行的,大家可以具體查看編譯生成的代碼。
文中示例代碼
到此這篇關(guān)于Fluent Mybatis實(shí)現(xiàn)環(huán)境隔離和租戶(hù)隔離的文章就介紹到這了,更多相關(guān)Fluent Mybatis環(huán)境隔離和租戶(hù)隔離內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java中的StringBuilder()常見(jiàn)方法詳解
StringBuilder是一個(gè)可變的字符序列,此類(lèi)提供一個(gè)與 StringBuffer 兼容的 API,但不保證同步,這篇文章主要介紹了StringBuilder()常見(jiàn)方法,需要的朋友可以參考下2023-09-09
使用Spring Framework 時(shí)常犯的十大錯(cuò)誤(小結(jié))
這篇文章主要介紹了使用Spring Framework 時(shí)常犯的十大錯(cuò)誤(小結(jié)),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-07-07
解決Idea的選擇文件后定位瞄準(zhǔn)器"Select Opened File"的功能
使用IntelliJ IDEA時(shí),可能會(huì)發(fā)現(xiàn)"SelectOpenedFile"功能不見(jiàn)了,這個(gè)功能允許用戶(hù)快速定位到當(dāng)前打開(kāi)文件的位置,若要找回此功能,只需在IDEA的標(biāo)題欄上右鍵,然后選擇"Always Select Opened File",這樣就可以重新啟用這個(gè)便捷的功能2024-11-11
Java實(shí)現(xiàn)英文句子中的單詞順序逆序輸出的方法
這篇文章主要介紹了Java實(shí)現(xiàn)英文句子中的單詞順序逆序輸出的方法,涉及java字符串遍歷、判斷、截取、輸出等相關(guān)操作技巧,需要的朋友可以參考下2018-01-01
Java架構(gòu)設(shè)計(jì)之六步拆解 DDD
DDD(Domain-Driven Design 領(lǐng)域驅(qū)動(dòng)設(shè)計(jì))是由Eric Evans最先提出,目的是對(duì)軟件所涉及到的領(lǐng)域進(jìn)行建模,以應(yīng)對(duì)系統(tǒng)規(guī)模過(guò)大時(shí)引起的軟件復(fù)雜性的問(wèn)題2022-02-02

