Fluent Mybatis实现环境隔离和租户隔离

目录
  • 什么是环境隔离和多租户隔离
  • 环境隔离和多租户隔离需要做的事情
  • 环境隔离和租户隔离工具类
  • 隔离前准备工作
  • 增删改查环境和租户隔离演示
    • 新增数据
    • 查询数据
    • 更新数据
  • 总结

什么是环境隔离和多租户隔离

我们在实际的业务开发中,经常会碰到环境逻辑隔离和租户数据逻辑隔离的问题。

环境隔离

我们的开发系统过程中,经常会涉及到日常开发环境,测试环境,预发环境和线上环境,如何区隔这些环境,有些方案是采用独立的数据库,有些是采用同一套数据库(比如线下多个测试环境使用同一个数据库,预发环境和线上环境使用同一个数据库),然后对数据进行打标的办法,来区分不同环境的数据。

多租户管理

在复杂的业务系统中,比如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. tenant, 表示数据所属租户,租户的隔离一般是通过登录用户信息获取的。

对环境和租户的隔离,主要是CRUD过程中,需要带上环境变量和租户信息。如果没有框架的支持,就需要在构造SQL的过程中,手动设置env和tenant。这就存在一个严重的弊端: 在编码过程中,需要时刻注意sql语句中不要漏了这2个条件,否则就会产生逻辑错误和信息泄露。

为了减少错误,我们都会将逻辑进行收拢,下面我们演示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隔离属性基类

为了方便对所有需要隔离的Entity进行统一的环境和租户信息的设置和读取,我们把Entity的环境和租户的属性的getter和setter方法定义到一个接口上。

/**
 * 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);
}

这样所有需要隔离的Entity只要继承这个接口就可以在需要隔离操作的地方把具体的entity当作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(), 我们把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接口。
接下来,我们进行具体的增删改查演示。

增删改查环境和租户隔离演示

新增数据

@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());
    }
}

查看控制台输出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

在演示例子中,我们虽然只显式设置了userName和address2个属性,但插入数据中设置了7个属性,其中包括env和tenant。
注意,这里的查询条件并没有带上环境变量

查询数据

fluent mybatis提供了2种构造查询器的方式

  1. XyzQuery.query(): 全新的不带任何条件的查询。
  2. XyzQuery.defaultQuery(): 按照@FluentMybatis defaults属性指定的接口,设置好默认查询条件。

上面默认插入的例子已经演示了不带条件的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());
    }
}

查看控制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

我们看到,查询条件中除了有我们设置好的user_name,还包括在IsolateSetter接口中设置好的env和tenant字段。

更新数据

和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()
        );
    }
}

查看控制台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

更新条件中自动带上了设置好的默认条件 env 和 tenant。

总结

Fluent Mybatis通过自定义接口继承IDefaultSetter,赋予了你进行数据隔离操作的强大功能。默认值的赋值是通过编译生成的XyzDefaults类来进行的,大家可以具体查看编译生成的代码。
文中示例代码

到此这篇关于Fluent Mybatis实现环境隔离和租户隔离的文章就介绍到这了,更多相关Fluent Mybatis环境隔离和租户隔离内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Springboot+mybatis-plus+注解实现数据权限隔离

    目录 1.创建注解 2. 具体实现 1.创建注解 当此注解打在类上,不需要传参,该类下所有查询接口开启数据隔离:打在方法上默认开启数据隔离,传参为false则该方法关闭验证 /** * 数据权限验证注解 * @author xiaohua * @date 2021/6/23 */ @Documented @Target({METHOD, ANNOTATION_TYPE, TYPE}) @Retention(RUNTIME) public @interface DataPermission { /

  • Fluent Mybatis实现环境隔离和租户隔离

    目录 什么是环境隔离和多租户隔离 环境隔离和多租户隔离需要做的事情 环境隔离和租户隔离工具类 隔离前准备工作 增删改查环境和租户隔离演示 新增数据 查询数据 更新数据 总结 什么是环境隔离和多租户隔离 我们在实际的业务开发中,经常会碰到环境逻辑隔离和租户数据逻辑隔离的问题. 环境隔离 我们的开发系统过程中,经常会涉及到日常开发环境,测试环境,预发环境和线上环境,如何区隔这些环境,有些方案是采用独立的数据库,有些是采用同一套数据库(比如线下多个测试环境使用同一个数据库,预发环境和线上环境使用同一个

  • Fluent Mybatis快速入门详细教程

    使用fluent mybatis可以不用写具体的xml文件,通过java api可以构造出比较复杂的业务sql语句,做到代码逻辑和sql逻辑的合一. 不再需要在Dao中组装查询或更新操作,在xml或mapper中再组装参数.喜欢的朋友可以阅读这篇文章   http://www.jb51.net/article/218884.htm 对底层数据表关联关系的处理,我们总是绕不开什么一对一,一对多,多对多这里比较烦人的关系. 业界优秀的ORM框架也都给出了自己的答案,简单来说就以下几种方式: hibe

  • Fluent MyBatis实现动态SQL

    目录 数据准备 代码生成 在 WHERE 条件中使用动态条件 在 UPDATE 使用动态更新 choose 标签 参考 MyBatis 令人喜欢的一大特性就是动态 SQL.在使用 JDBC 的过程中, 根据条件进行 SQL 的拼接是很麻烦且很容易出错的, MyBatis虽然提供了动态拼装的能力,但这些写xml文件,也确实折磨开发.Fluent MyBatis提供了更贴合Java语言特质的,对程序员友好的Fluent拼装能力. Fluent MyBatis动态SQL,写SQL更爽 数据准备 为了后

  • Fluent Mybatis零xml配置实现复杂嵌套查询

    目录 嵌套查询 in (select 子查询) exists (select子查询) 嵌套查询 使用Fluent Mybatis, 不用手写一行xml文件或者Mapper文件,在dao类中即可使用java api构造中比较复杂的嵌套查询. 让dao的代码逻辑和sql逻辑合二为一. 前置准备,maven工程设置 参考文章 使用FluentMybatis实现mybatis动态sql拼装和fluent api语法 in (select 子查询) 嵌套查询表和主查询表一样的场景 .column().in

  • Fluent Mybatis如何做到代码逻辑和sql逻辑的合一

    使用fluent mybatis可以不用写具体的xml文件,通过java api可以构造出比较复杂的业务sql语句,做到代码逻辑和sql逻辑的合一.不再需要在Dao中组装查询或更新操作,在xml或mapper中再组装参数.那对比原生Mybatis, Mybatis Plus或者其他框架,FluentMybatis提供了哪些便利呢? 场景需求设置 我们通过一个比较典型的业务需求来具体实现和对比下,假如有学生成绩表结构如下: create table `student_score` ( id big

  • Fluent Mybatis让你摆脱Xml文件的技巧

    目录 一.啥是Fluent-Mybatis 二.SpringBoot + Fluent-Mybatis 三.官方链接 一.啥是Fluent-Mybatis 与Mybatis-Plus类似,是对Mybaits进一步的封装,使之语法简洁明了,更重要的是不需要在自主创建Xml文件,可以只用一个实体类对象,通过代码生成器,在编译的过程中生成所需要的各类文件,简化了项目的基础构建,提高开发效率. 二.SpringBoot + Fluent-Mybatis 1.创建数据库测试表 DROP TABLE IF

  • Fluent Mybatis实际开发中的优势对比

    之前文章介绍过了Fluent基本框架等,其中有几个重要的方法用到了IQuery和IUpdate对象. 这2个对象是FluentMybatis实现复杂和动态sql的构造类,通过这2个对象fluent mybatis可以不用写具体的xml文件, 直接通过java api可以构造出比较复杂的业务sql语句,做到代码逻辑和sql逻辑的合一.下面接着介绍如何通过IQuery和IUpdate定义强大的动态SQL语句. 表结构 假如有学生成绩表结构如下: create table `student_score

  • Fluent Mybatis,原生Mybatis,Mybatis Plus三者功能对比

    目录 三者实现对比 使用fluent mybatis 来实现上面的功能 换成mybatis原生实现效果 换成mybatis plus实现效果 生成代码编码比较 fluent mybatis生成代码设置 mybatis plus代码生成设置 FluentMybatis特性一览 三者对比总结 Fluent Mybatis介绍和源码 使用fluent mybatis可以不用写具体的xml文件,通过java api可以构造出比较复杂的业务sql语句,做到代码逻辑和sql逻辑的合一. 不用再需要在Dao中

  • springboot 整合fluent mybatis的过程,看这篇够了

    1.导入pom依赖 <!-- mybatis--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.1</version> </dependency> <!--mysql依赖--> <de

  • Fluent Mybatis 批量更新的使用

    目录 批量更新同一张表的数据 更新多条数据,每条数据都不一样 java中for循环实现方式 一条SQL,服务端逐条更新 mybatis实现方式 使用FluentMybatis实现方式 使用mysql的Case When then方式更新 mybatis原生实现方式 批量更新不同的表数据 参考 批量更新同一张表的数据 更新多条数据,每条数据都不一样 背景描述 通常需要一次更新多条数据有两个方式 在业务代码中循环遍历,逐条更新 一次性更新所有数据, 采用批量sql方式,一次执行. 更准确的说是一条s

随机推荐