Введение в Fluent Mybatis Введение пятое: изоляция среды и изоляция арендатора

Java

Что такое изоляция среды и многопользовательская изоляция

В реальном развитии бизнеса мы часто сталкиваемся с проблемой изоляции логики среды и логики данных арендатора.

  • Экологическая изоляция

В процессе разработки нашей системы мы часто задействуем среду ежедневной разработки, тестовую среду, предрелизную среду и онлайн-среду.Как разделить эти среды?Некоторые схемы используют независимые базы данных, а некоторые используют один и тот же набор баз данных (например, автономные несколько тестовых сред используют одну и ту же базу данных, а предварительная версия и онлайн-среда используют одну и ту же базу данных), а затем пометьте данные, чтобы различать данные из разных сред.

  • Управление несколькими арендаторами

Как в сложных бизнес-системах, таких как приложения 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 полей

  1. env представляет среду, в которой развернуто приложение, и среда обычно отличается переменными среды компьютера развертывания приложения.
  2. арендатор, указывает арендатора, которому принадлежат данные.Изоляция арендаторов обычно достигается путем входа в систему информации о пользователе.

Изоляция среды и арендатора, в основном в процессе 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 способа создания запросов

  1. XyzQuery.query(): новый запрос без каких-либо условий.
  2. 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.

  1. XyzUpdate.updater() : Обновление без каких-либо условий.
  2. 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

Введение в Fluent MyBatis

Введение в FluentMybatis II

Введение 3: Сложный запрос и запрос с объединенной таблицей

Введение в FluentMybatis Four: многие ко многим

Сравнение функций Fluent Mybatis, родного Mybatis и Mybatis Plus

  • свободная документация mybatis и исходный код

Свободная документация и примеры Mybatis

Fluent Mybatis Gitee

Fluent Mybatis GitHub