Что такое изоляция среды и многопользовательская изоляция
В реальном развитии бизнеса мы часто сталкиваемся с проблемой изоляции логики среды и логики данных арендатора.
- Экологическая изоляция
В процессе разработки нашей системы мы часто задействуем среду ежедневной разработки, тестовую среду, предрелизную среду и онлайн-среду.Как разделить эти среды?Некоторые схемы используют независимые базы данных, а некоторые используют один и тот же набор баз данных (например, автономные несколько тестовых сред используют одну и ту же базу данных, а предварительная версия и онлайн-среда используют одну и ту же базу данных), а затем пометьте данные, чтобы различать данные из разных сред.
- Управление несколькими арендаторами
Как в сложных бизнес-системах, таких как приложения SaaS, обеспечить изоляцию данных между пользователями за счет совместного использования одних и тех же системных или программных компонентов в многопользовательской среде? Проще говоря: один экземпляр приложения, работающий на сервере, который обслуживает несколько арендаторов (клиентов). Из определения мы можем понять, что мультиарендность — это архитектура, предназначенная для использования одного и того же набора программ в многопользовательской среде, но для обеспечения изоляции данных между пользователями. Ключевым моментом реализации мультитенантности является изоляция многопользовательских данных в рамках одного и того же набора программ Практика фактически такая же, как и изоляция среды.
Здесь мы используем сценарий совместного использования таблицы данных с несколькими средами и несколькими арендаторами, чтобы обсудить, как FluentMybatis поддерживает управление несколькими средами и несколькими арендаторами.
Что необходимо сделать для изоляции от окружающей среды и многопользовательской изоляции
Например, у нас есть следующая таблица
create table student
(
id bigint(21) unsigned auto_increment comment '主键id'
primary key,
age int null comment '年龄',
grade int null comment '年级',
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 '电话',
bonus_points bigint(21) default 0 null comment '积分',
status varchar(32) null comment '状态(字典)',
home_county_id bigint(21) null comment '家庭所在区县',
home_address_id bigint(21) null comment 'home_address外键',
address varchar(200) null comment '家庭详细住址',
version varchar(200) null comment '版本号',
env varchar(10) NULL comment '数据隔离环境',
tenant bigint NOT NULL default 0 comment '租户标识',
gmt_created datetime null comment '创建时间',
gmt_modified datetime null comment '更新时间',
is_deleted tinyint(2) default 0 null comment '是否逻辑删除'
) ENGINE = InnoDB
DEFAULT CHARSET = utf8
COMMENT '学生信息表';
Примечание 2 полей
- env представляет среду, в которой развернуто приложение, и среда обычно отличается переменными среды компьютера развертывания приложения.
- арендатор, указывает арендатора, которому принадлежат данные.Изоляция арендаторов обычно достигается путем входа в систему информации о пользователе.
Изоляция среды и арендатора, в основном в процессе CRUD, требует внесения переменных среды и информации об арендаторе. Если нет поддержки фреймворка, вам нужно вручную установить env и tenant в процессе построения SQL. У этого есть серьезный недостаток: в процессе кодирования необходимо всегда обращать внимание на то, чтобы не пропустить эти два условия в операторе SQL, иначе возникнут логические ошибки и утечка информации.
Чтобы уменьшить количество ошибок, мы сконденсируем логику.Ниже мы покажем, как fluent mybatis справляется с ней единообразно.
Средства изоляции среды и изоляции арендаторов
Для изоляции среды и изоляции арендаторов мы обычно определяем классы инструментов для унифицированного получения переменных среды и информации об арендаторах.
- Инструменты изоляции среды
/**
* 应用部署环境工具类
*/
public class EnvUtils {
public static String currEnv() {
// 应用启动时, 读取的机器部署环境变量, 这里简化为返回固定值演示
return "test1";
}
}
- Класс инструмента изоляции арендатора
/**
* 获取用户所属租户信息工具类
*/
public class TenantUtils {
/**
* 租户A
*/
static final long A_TENANT = 111111L;
/**
* 租户B
*/
static final long B_TENANT = 222222L;
/**
* 租户信息一般根据登录用户身份来判断, 这里简化为偶数用户属于租户A, 奇数用户属于租户B
*
* @return
*/
public static long findUserTenant() {
long userId = loginUserId();
if (userId % 2 == 0) {
return A_TENANT;
} else {
return B_TENANT;
}
}
/**
* 当前登录的用户id, 一般从Session中获取
*
* @return
*/
public static long loginUserId() {
return 1L;
}
}
Подготовка к изоляции
- Базовый класс свойств изоляции объектов
Чтобы облегчить унифицированную настройку и чтение информации о среде и клиенте для всех сущностей, которые необходимо изолировать, мы определяем методы получения и установки среды сущности и свойств арендатора на интерфейсе.
/**
* Entity类隔离属性基类
*/
public interface IsolateEntity {
/**
* 返回entity env属性值
*
* @return
*/
String getEnv();
/**
* 设置entity env属性值
*
* @param env
* @return
*/
IsolateEntity setEnv(String env);
/**
* 返回entity 租户信息
*
* @return
*/
Long getTenant();
/**
* 设置entity 租户信息
*
* @param tenant
* @return
*/
IsolateEntity setTenant(Long tenant);
}
Таким образом, все объекты, которые необходимо изолировать, могут использовать конкретный объект в качестве объекта IsolateEntity для работы там, где требуются операции изоляции, если они наследуют этот интерфейс.
- Свойства изоляции и настройки условий по умолчанию
С унифицированным интерфейсом нам также нужна операция, которая устанавливается по умолчанию. Fluent mybatis предоставляет интерфейс IDefaultSetter, который может перехватывать Entity, Query и Update.
/**
* 增删改查中,环境和租户隔离设置
*/
public interface IsolateSetter extends IDefaultSetter {
/**
* 插入的entity,如果没有显式设置环境和租户,根据工具类进行默认设置
*
* @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());
}
}
/**
* 查询条件追加环境隔离和租户隔离
*
* @param query
*/
@Override
default void setQueryDefault(IQuery query) {
query.where()
.apply("env", SqlOp.EQ, EnvUtils.currEnv())
.apply("tenant", SqlOp.EQ, TenantUtils.findUserTenant());
}
/**
* 更新条件追加环境隔离和租户隔离
*
* @param updater
*/
@Override
default void setUpdateDefault(IUpdate updater) {
updater.where()
.apply("env", SqlOp.EQ, EnvUtils.currEnv())
.apply("tenant", SqlOp.EQ, TenantUtils.findUserTenant());
}
}
Чтобы избежать проблем с безопасностью потоков (совместное использование переменных), вызванных неправильным использованием, fluent mybatis позволяет только интерфейсам (таким как IsolateSetter здесь) наследовать IDefaultSetter в приложении и не может быть определен как класс.
- Настройки генерации кода
Как позволить fluent mybatis определить, какие Entity могут наследовать IsolateEntity и какие операции Entity должны быть единообразно перехвачены IsolateSetter?
В @FluentMybatis есть свойство defaults(), мы можем установить значение по умолчанию IsolateSetter.class.
public @interface FluentMybatis {
/**
* entity, query, updater默认值设置实现
*
* @return
*/
Class<? extends IDefaultSetter> defaults() default IDefaultSetter.class;
}
Конечно, нам не нужно вручную изменять класс Entity, нам нужно только установить его при генерации кода.
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(
/** 数据库连接信息 **/
url = url, username = "root", password = "password",
/** Entity类parent package路径 **/
basePack = basePackage,
/** Entity代码源目录 **/
srcDir = "example/many2many_demo/src/main/java",
/** 如果表定义记录创建,记录修改,逻辑删除字段 **/
gmtCreated = "gmt_created", gmtModified = "gmt_modified", logicDeleted = "is_deleted",
/** 需要生成文件的表 ( 表名称:对应的Entity名称 ) **/
tables = @Table(value = {"student"},
entity = IsolateEntity.class,
defaults = IsolateSetter.class)
)
static class Noting {
}
}
Обратите внимание, что по сравнению с предыдущим поколением кода в @Table есть еще 2 параметра свойства.
// 标识对应的Entity类需要继承的接口
entity = IsolateEntity.class
// 标识对应的Entity类CRUD过程中需要进行的默认设置操作
defaults = IsolateSetter.class
Выполните генерацию кода, код Entity выглядит следующим образом:
@FluentMybatis(
table = "student",
defaults = IsolateSetter.class
)
public class StudentEntity extends RichEntity implements IsolateEntity {
// ... 省略
}
Мы видим, что @FluentMybatis устанавливает свойство defaults, а класс Entity наследует интерфейс IsolateEntity.
Далее мы проведем конкретную демонстрацию добавлений, удалений и изменений.
Демонстрация среды CRUD и изоляции арендаторов
Добавить данные
@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());
}
}
Просмотр журнала вывода консоли
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
В демонстрационном примере, хотя мы явно установили только 2 атрибута userName и address, во вставленных данных установлено 7 атрибутов, включая env и tenant.
Обратите внимание, что условия запроса здесь не приносят переменные среды
Данные запроса
fluent mybatis предоставляет 2 способа создания запросов
- XyzQuery.query(): новый запрос без каких-либо условий.
- XyzQuery.defaultQuery(): установите условия запроса по умолчанию в соответствии с интерфейсом, заданным атрибутом @FluentMybatis по умолчанию.
Приведенный выше пример вставки по умолчанию продемонстрировал запрос query() без условий, теперь мы демонстрируем запрос с установленными условиями по умолчанию.
@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());
}
}
Просмотр выходных данных журнала управления
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
Мы видим, что в дополнение к установленному нами user_name, условия запроса также включают поля env и tenant, установленные в интерфейсе IsolateSetter.
обновить данные
Как и Query, Updater также предоставляет 2 метода для создания Updater.
- XyzUpdate.updater() : Обновление без каких-либо условий.
- XyzUpdate.defaultUpdater(): установите условия обновления в соответствии с методом IsolateSetter#setUpdateDefault.
Демонстрационный пример
@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()
);
}
}
Просмотр вывода журнала консоли
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
Установленные по умолчанию условия env и tenant автоматически включаются в условия обновления.
Суммировать
Fluent Mybatis наследует IDefaultSetter через настраиваемый интерфейс, предоставляя вам мощные функции для операций изоляции данных. Присвоение значения по умолчанию выполняется путем компиляции сгенерированного класса XyzDefaults.Подробности можно проверить в скомпилированном коде.Пример кода в тексте
- беглый цикл статей mybatis
Введение 3: Сложный запрос и запрос с объединенной таблицей
Введение в FluentMybatis Four: многие ко многим
Сравнение функций Fluent Mybatis, родного Mybatis и Mybatis Plus
- свободная документация mybatis и исходный код