MyBatis-Flex一个优雅的 MyBatis 增强框架。
更轻量
MyBatis-Flex 除了 MyBatis 本身,再无任何第三方依赖,因此会带来更高的自主性、把控性和稳定性。在任何一个系统中,依赖越多,稳定性越差。
更灵活
MyBatis-Flex 提供了非常灵活的 QueryWrapper,支持关联查询、多表查询、多主键、逻辑删除、乐观锁更新、数据填充、数据脱敏、等等….
更高的性能
MyBatis-Flex 通过独特的架构,没有任何 MyBatis 拦截器、在 SQL 执行的过程中,没有任何的 SQL Parse,因此会带来指数级的性能增长。
官网提供的和同类框架的功能对比
功能或特点 | MyBatis-Flex | MyBatis-Plus | Fluent-MyBatis |
---|---|---|---|
对 entity 的基本增删改查 | ✅ | ✅ | ✅ |
分页查询 | ✅ | ✅ | ✅ |
分页查询之总量缓存 | ✅ | ✅ | ❌ |
分页查询无 SQL 解析设计(更轻量,及更高性能) | ✅ | ❌ | ✅ |
多表查询: from 多张表 | ✅ | ❌ | ❌ |
多表查询: left join、inner join 等等 | ✅ | ❌ | ✅ |
多表查询: union,union all | ✅ | ❌ | ✅ |
单主键配置 | ✅ | ✅ | ✅ |
多种 id 生成策略 | ✅ | ✅ | ✅ |
支持多主键、复合主键 | ✅ | ❌ | ❌ |
字段的 typeHandler 配置 | ✅ | ✅ | ✅ |
除了 MyBatis,无其他第三方依赖(更轻量) | ✅ | ❌ | ❌ |
QueryWrapper 是否支持在微服务项目下进行 RPC 传输 | ✅ | ❌ | 未知 |
逻辑删除 | ✅ | ✅ | ✅ |
乐观锁 | ✅ | ✅ | ✅ |
SQL 审计 | ✅ | ❌ | ❌ |
数据填充 | ✅ | ✔️ (收费) | ✅ |
数据脱敏 | ✅ | ✔️ (收费) | ❌ |
字段权限 | ✅ | ✔️ (收费) | ❌ |
字段加密 | ✅ | ✔️ (收费) | ❌ |
字典回写 | ✅ | ✔️ (收费) | ❌ |
Db + Row | ✅ | ❌ | ❌ |
Entity 监听 | ✅ | ❌ | ❌ |
多数据源支持 | ✅ | 借助其他框架或收费 | ❌ |
多数据源是否支持 Spring 的事务管理,比如 @Transactional 和 TransactionTemplate 等 |
✅ | ❌ | ❌ |
多数据源是否支持 “非Spring” 项目 | ✅ | ❌ | ❌ |
多租户 | ✅ | ✅ | ❌ |
动态表名 | ✅ | ✅ | ❌ |
动态 Schema | ✅ | ❌ | ❌ |
官网提供的和同类框架的性能对比
- MyBatis-Flex 的查询单条数据的速度,大概是 MyBatis-Plus 的 5 ~ 10+ 倍。
- MyBatis-Flex 的查询 10 条数据的速度,大概是 MyBatis-Plus 的 5~10 倍左右。
- Mybatis-Flex 的分页查询速度,大概是 Mybatis-Plus 的 5~10 倍左右。
- Mybatis-Flex 的数据更新速度,大概是 Mybatis-Plus 的 5~10+ 倍。
亮点功能
除了Mybatis-plus带的那些功能,Mybatis-Flex提供了多主键、复合主键功能;提供了关联查询;特别是关联查询在日常业务开发碰到的场景很多。
Mybatis-Flex提供了一对一、一对多、多对一、多对多的场景。
一对一关联查询 @RelationOneToOne
假设有一个账户,账户有身份证,账户和身份证的关系是一对一的关系,代码如下所示:
Account.java :
public class Account implements Serializable {@Id(keyType = KeyType.Auto)private Long id;private String userName;@RelationOneToOne(selfField = "id", targetField = "accountId")private IDCard idCard;//getter setter}public class Account implements Serializable { @Id(keyType = KeyType.Auto) private Long id; private String userName; @RelationOneToOne(selfField = "id", targetField = "accountId") private IDCard idCard; //getter setter }public class Account implements Serializable { @Id(keyType = KeyType.Auto) private Long id; private String userName; @RelationOneToOne(selfField = "id", targetField = "accountId") private IDCard idCard; //getter setter }
IDCard.java :
@Table(value = "tb_idcard")public class IDCard implements Serializable {private Long accountId;private String cardNo;private String content;//getter setter}@Table(value = "tb_idcard") public class IDCard implements Serializable { private Long accountId; private String cardNo; private String content; //getter setter }@Table(value = "tb_idcard") public class IDCard implements Serializable { private Long accountId; private String cardNo; private String content; //getter setter }
@RelationOneToOne
配置描述:
- selfField 当前实体类的属性
- targetField 目标对象的关系实体类的属性
PS: 若 selfField 是主键,且当前表只有 1 个主键时,可以不填写。因此,以上的配置可以简化为
@RelationOneToOne(targetField = "accountId")
假设数据库 5 条 Account 数据,然后进行查询:
List<Account> accounts = accountMapper.selectAllWithRelations();System.out.println(accounts);List<Account> accounts = accountMapper.selectAllWithRelations(); System.out.println(accounts);List<Account> accounts = accountMapper.selectAllWithRelations(); System.out.println(accounts);
其执行的 SQL 如下:
SELECT `id`, `user_name`, `age` FROM `tb_account`SELECT `account_id`, `card_no`, `content` FROM `tb_idcard`WHERE account_id IN (1, 2, 3, 4, 5)SELECT `id`, `user_name`, `age` FROM `tb_account` SELECT `account_id`, `card_no`, `content` FROM `tb_idcard` WHERE account_id IN (1, 2, 3, 4, 5)SELECT `id`, `user_name`, `age` FROM `tb_account` SELECT `account_id`, `card_no`, `content` FROM `tb_idcard` WHERE account_id IN (1, 2, 3, 4, 5)
查询打印的结果如下:
[Account{id=1, userName='孙悟空', age=18, idCard=IDCard{accountId=1, cardNo='0001', content='内容1'}},Account{id=2, userName='猪八戒', age=19, idCard=IDCard{accountId=2, cardNo='0002', content='内容2'}},Account{id=3, userName='沙和尚', age=19, idCard=IDCard{accountId=3, cardNo='0003', content='内容3'}},Account{id=4, userName='六耳猕猴', age=19, idCard=IDCard{accountId=4, cardNo='0004', content='内容4'}},Account{id=5, userName='王麻子叔叔', age=19, idCard=IDCard{accountId=5, cardNo='0005', content='内容5'}}][ Account{id=1, userName='孙悟空', age=18, idCard=IDCard{accountId=1, cardNo='0001', content='内容1'}}, Account{id=2, userName='猪八戒', age=19, idCard=IDCard{accountId=2, cardNo='0002', content='内容2'}}, Account{id=3, userName='沙和尚', age=19, idCard=IDCard{accountId=3, cardNo='0003', content='内容3'}}, Account{id=4, userName='六耳猕猴', age=19, idCard=IDCard{accountId=4, cardNo='0004', content='内容4'}}, Account{id=5, userName='王麻子叔叔', age=19, idCard=IDCard{accountId=5, cardNo='0005', content='内容5'}} ][ Account{id=1, userName='孙悟空', age=18, idCard=IDCard{accountId=1, cardNo='0001', content='内容1'}}, Account{id=2, userName='猪八戒', age=19, idCard=IDCard{accountId=2, cardNo='0002', content='内容2'}}, Account{id=3, userName='沙和尚', age=19, idCard=IDCard{accountId=3, cardNo='0003', content='内容3'}}, Account{id=4, userName='六耳猕猴', age=19, idCard=IDCard{accountId=4, cardNo='0004', content='内容4'}}, Account{id=5, userName='王麻子叔叔', age=19, idCard=IDCard{accountId=5, cardNo='0005', content='内容5'}} ]
一对多关联查询 @RelationOneToMany
假设一个账户有很多本书籍,一本书只能归属一个账户所有;账户和书籍的关系是一对多的关系,代码如下:
Account.java :
public class Account implements Serializable {@Id(keyType = KeyType.Auto)private Long id;private String userName;@RelationOneToMany(selfField = "id", targetField = "accountId")private List<Book> books;//getter setter}public class Account implements Serializable { @Id(keyType = KeyType.Auto) private Long id; private String userName; @RelationOneToMany(selfField = "id", targetField = "accountId") private List<Book> books; //getter setter }public class Account implements Serializable { @Id(keyType = KeyType.Auto) private Long id; private String userName; @RelationOneToMany(selfField = "id", targetField = "accountId") private List<Book> books; //getter setter }
Book.java :
@Table(value = "tb_book")public class Book implements Serializable {@Id(keyType = KeyType.Auto)private Long id;private Long accountId;private String title;//getter setter}@Table(value = "tb_book") public class Book implements Serializable { @Id(keyType = KeyType.Auto) private Long id; private Long accountId; private String title; //getter setter }@Table(value = "tb_book") public class Book implements Serializable { @Id(keyType = KeyType.Auto) private Long id; private Long accountId; private String title; //getter setter }
@RelationOneToMany
配置描述:
- selfField 当前实体类的属性
- targetField 目标对象的关系实体类的属性
PS: 若 selfField 是主键,且当前表只有 1 个主键时,可以不填写。因此,以上的配置可以简化为
@RelationOneToOne(targetField = "accountId")
假设数据库 5 条 Account 数据,然后进行查询:
List<Account> accounts = accountMapper.selectAllWithRelations();System.out.println(accounts);List<Account> accounts = accountMapper.selectAllWithRelations(); System.out.println(accounts);List<Account> accounts = accountMapper.selectAllWithRelations(); System.out.println(accounts);
其执行的 SQL 如下:
SELECT `id`, `user_name`, `age` FROM `tb_account`SELECT `id`, `account_id`, `title`, `content` FROM `tb_book`WHERE account_id IN (1, 2, 3, 4, 5)SELECT `id`, `user_name`, `age` FROM `tb_account` SELECT `id`, `account_id`, `title`, `content` FROM `tb_book` WHERE account_id IN (1, 2, 3, 4, 5)SELECT `id`, `user_name`, `age` FROM `tb_account` SELECT `id`, `account_id`, `title`, `content` FROM `tb_book` WHERE account_id IN (1, 2, 3, 4, 5)
Map 映射
若 Account.books
是一个 Map
,而非 List
,那么,我们需要通过配置 mapKeyField
来指定使用用个列来充当 Map
的 Key
, 如下代码所示:
java
public class Account implements Serializable {@Id(keyType = KeyType.Auto)private Long id;private String userName;@RelationOneToMany(selfField = "id", targetField = "accountId", mapKeyField = "id") //使用 Book 的 id 来填充这个 map 的 keyprivate Map<Long, Book> books;//getter setter}public class Account implements Serializable { @Id(keyType = KeyType.Auto) private Long id; private String userName; @RelationOneToMany(selfField = "id", targetField = "accountId" , mapKeyField = "id") //使用 Book 的 id 来填充这个 map 的 key private Map<Long, Book> books; //getter setter }public class Account implements Serializable { @Id(keyType = KeyType.Auto) private Long id; private String userName; @RelationOneToMany(selfField = "id", targetField = "accountId" , mapKeyField = "id") //使用 Book 的 id 来填充这个 map 的 key private Map<Long, Book> books; //getter setter }
多对多注解
@RelationManyToMany
也是如此。
多对一关联查询 @RelationManyToOne
假设一个账户有很多本书籍,一本书只能归属一个账户所有;账户和书籍的关系是一对多的关系,书籍和账户的关系为多对一的关系,代码如下:
Account.java:
public class Account implements Serializable {@Id(keyType = KeyType.Auto)private Long id;private String userName;//getter setter}public class Account implements Serializable { @Id(keyType = KeyType.Auto) private Long id; private String userName; //getter setter }public class Account implements Serializable { @Id(keyType = KeyType.Auto) private Long id; private String userName; //getter setter }
Book.java 多对一的配置:
@Table(value = "tb_book")public class Book implements Serializable {@Id(keyType = KeyType.Auto)private Long id;private Long accountId;private String title;@RelationManyToOne(selfField = "accountId", targetField = "id")private Account account;//getter setter}@Table(value = "tb_book") public class Book implements Serializable { @Id(keyType = KeyType.Auto) private Long id; private Long accountId; private String title; @RelationManyToOne(selfField = "accountId", targetField = "id") private Account account; //getter setter }@Table(value = "tb_book") public class Book implements Serializable { @Id(keyType = KeyType.Auto) private Long id; private Long accountId; private String title; @RelationManyToOne(selfField = "accountId", targetField = "id") private Account account; //getter setter }
@RelationManyToOne
配置描述:
- selfField 当前实体类的属性
- targetField 目标对象的关系实体类的属性
PS: 若 targetField 目标对象的是主键,且目标对象的表只有 1 个主键时,可以不填写。因此,以上的配置可以简化为
@RelationManyToOne(selfField = "accountId")
多对多关联查询 @RelationManyToMany
假设一个账户可以有多个角色,一个角色也可以有多个账户,他们是多对多的关系,需要通过中间件表 tb_role_mapping
来维护:
tb_role_mapping
的表结构如下:
CREATE TABLE `tb_role_mapping`(`account_id` INTEGER ,`role_id` INTEGER);CREATE TABLE `tb_role_mapping` ( `account_id` INTEGER , `role_id` INTEGER );CREATE TABLE `tb_role_mapping` ( `account_id` INTEGER , `role_id` INTEGER );
Account.java 多对多的配置:
public class Account implements Serializable {@Id(keyType = KeyType.Auto)private Long id;private String userName;@RelationManyToMany(joinTable = "tb_role_mapping", // 中间表selfField = "id", joinSelfColumn = "account_id",targetField = "id", joinTargetColumn = "role_id")private List<Role> roles;//getter setter}public class Account implements Serializable { @Id(keyType = KeyType.Auto) private Long id; private String userName; @RelationManyToMany( joinTable = "tb_role_mapping", // 中间表 selfField = "id", joinSelfColumn = "account_id", targetField = "id", joinTargetColumn = "role_id" ) private List<Role> roles; //getter setter }public class Account implements Serializable { @Id(keyType = KeyType.Auto) private Long id; private String userName; @RelationManyToMany( joinTable = "tb_role_mapping", // 中间表 selfField = "id", joinSelfColumn = "account_id", targetField = "id", joinTargetColumn = "role_id" ) private List<Role> roles; //getter setter }
Role.java 多对多的配置:
@Table(value = "tb_role")public class Role implements Serializable {private Long id;private String name;//getter setter}@Table(value = "tb_role") public class Role implements Serializable { private Long id; private String name; //getter setter }@Table(value = "tb_role") public class Role implements Serializable { private Long id; private String name; //getter setter }
@RelationManyToMany
配置描述:
- selfField 当前实体类的属性
- targetField 目标对象的关系实体类的属性
- joinTable 中间表
- joinSelfColumn 当前表和中间表的关系字段
- joinTargetColumn 目标表和中间表的关系字段
注意:selfField 和 targetField 配置的是类的属性名,joinSelfColumn 和 joinTargetColumn 配置的是中间表的字段名。
若 selfField 和 targetField 分别是两张关系表的主键,且表只有 1 个主键时,可以不填写。因此,以上配置可以简化如下:
public class Account implements Serializable {@Id(keyType = KeyType.Auto)private Long id;private String userName;@RelationManyToMany(joinTable = "tb_role_mapping", // 中间表joinSelfColumn = "account_id",joinTargetColumn = "role_id")private List<Role> roles;//getter setter}public class Account implements Serializable { @Id(keyType = KeyType.Auto) private Long id; private String userName; @RelationManyToMany( joinTable = "tb_role_mapping", // 中间表 joinSelfColumn = "account_id", joinTargetColumn = "role_id" ) private List<Role> roles; //getter setter }public class Account implements Serializable { @Id(keyType = KeyType.Auto) private Long id; private String userName; @RelationManyToMany( joinTable = "tb_role_mapping", // 中间表 joinSelfColumn = "account_id", joinTargetColumn = "role_id" ) private List<Role> roles; //getter setter }
父子关系查询
比如在一些系统中,比如菜单会有一些父子关系,例如菜单表如下:
CREATE TABLE `tb_menu`(`id` INTEGER auto_increment,`parent_id` INTEGER,`name` VARCHAR(100));CREATE TABLE `tb_menu` ( `id` INTEGER auto_increment, `parent_id` INTEGER, `name` VARCHAR(100) );CREATE TABLE `tb_menu` ( `id` INTEGER auto_increment, `parent_id` INTEGER, `name` VARCHAR(100) );
Menu.java 定义如下:
@Table(value = "tb_menu")public class Menu implements Serializable {private Long id;private Long parentId;private String name;@RelationManyToOne(selfField = "parentId", targetField = "id")private Menu parent;@RelationOneToMany(selfField = "id", targetField = "parentId")private List<Menu> children;//getter setter}@Table(value = "tb_menu") public class Menu implements Serializable { private Long id; private Long parentId; private String name; @RelationManyToOne(selfField = "parentId", targetField = "id") private Menu parent; @RelationOneToMany(selfField = "id", targetField = "parentId") private List<Menu> children; //getter setter }@Table(value = "tb_menu") public class Menu implements Serializable { private Long id; private Long parentId; private String name; @RelationManyToOne(selfField = "parentId", targetField = "id") private Menu parent; @RelationOneToMany(selfField = "id", targetField = "parentId") private List<Menu> children; //getter setter }
查询顶级菜单:
QueryWrapper qw = QueryWrapper.create();qw.where(MENU.PARENT_ID.eq(0));List<Menu> menus = menuMapper.selectListWithRelationsByQuery(qw);System.out.println(JSON.toJSONString(menus));QueryWrapper qw = QueryWrapper.create(); qw.where(MENU.PARENT_ID.eq(0)); List<Menu> menus = menuMapper.selectListWithRelationsByQuery(qw); System.out.println(JSON.toJSONString(menus));QueryWrapper qw = QueryWrapper.create(); qw.where(MENU.PARENT_ID.eq(0)); List<Menu> menus = menuMapper.selectListWithRelationsByQuery(qw); System.out.println(JSON.toJSONString(menus));
SQL 执行如下:
SELECT `id`, `parent_id`, `name` FROM `tb_menu` WHERE `parent_id` = 0SELECT `id`, `parent_id`, `name` FROM `tb_menu` WHERE id = 0SELECT `id`, `parent_id`, `name` FROM `tb_menu` WHERE parent_id IN (1, 2, 3)SELECT `id`, `parent_id`, `name` FROM `tb_menu` WHERE `parent_id` = 0 SELECT `id`, `parent_id`, `name` FROM `tb_menu` WHERE id = 0 SELECT `id`, `parent_id`, `name` FROM `tb_menu` WHERE parent_id IN (1, 2, 3)SELECT `id`, `parent_id`, `name` FROM `tb_menu` WHERE `parent_id` = 0 SELECT `id`, `parent_id`, `name` FROM `tb_menu` WHERE id = 0 SELECT `id`, `parent_id`, `name` FROM `tb_menu` WHERE parent_id IN (1, 2, 3)
JSON 输出内容如下:
[ { "children": [ { "id": 4, "name": "子菜单", "parentId": 1 }, { "id": 5, "name": "子菜单", "parentId": 1 } ],"id": 1,"name": "顶级菜单1","parentId": 0},{"children": [],"id": 2,"name": "顶级菜单2","parentId": 0},{"children": [{"id": 6,"name": "子菜单","parentId": 3},{"id": 7,"name": "子菜单","parentId": 3},{"id": 8,"name": "子菜单","parentId": 3}],"id": 3,"name": "顶级菜单3","parentId": 0}][ { "children": [ { "id": 4, "name": "子菜单", "parentId": 1 }, { "id": 5, "name": "子菜单", "parentId": 1 } ], "id": 1, "name": "顶级菜单1", "parentId": 0 }, { "children": [], "id": 2, "name": "顶级菜单2", "parentId": 0 }, { "children": [ { "id": 6, "name": "子菜单", "parentId": 3 }, { "id": 7, "name": "子菜单", "parentId": 3 }, { "id": 8, "name": "子菜单", "parentId": 3 } ], "id": 3, "name": "顶级菜单3", "parentId": 0 } ][ { "children": [ { "id": 4, "name": "子菜单", "parentId": 1 }, { "id": 5, "name": "子菜单", "parentId": 1 } ], "id": 1, "name": "顶级菜单1", "parentId": 0 }, { "children": [], "id": 2, "name": "顶级菜单2", "parentId": 0 }, { "children": [ { "id": 6, "name": "子菜单", "parentId": 3 }, { "id": 7, "name": "子菜单", "parentId": 3 }, { "id": 8, "name": "子菜单", "parentId": 3 } ], "id": 3, "name": "顶级菜单3", "parentId": 0 } ]
在以上的父子关系查询中,默认的递归查询深度为 3 个层级,若需要查询指定递归深度,需要添加如下配置:
QueryWrapper qw = QueryWrapper.create();qw.where(MENU.PARENT_ID.eq(0));//设置递归查询深度为 10 层RelationManager.setMaxDepth(10);List<Menu> menus = menuMapper.selectListWithRelationsByQuery(qw);QueryWrapper qw = QueryWrapper.create(); qw.where(MENU.PARENT_ID.eq(0)); //设置递归查询深度为 10 层 RelationManager.setMaxDepth(10); List<Menu> menus = menuMapper.selectListWithRelationsByQuery(qw);QueryWrapper qw = QueryWrapper.create(); qw.where(MENU.PARENT_ID.eq(0)); //设置递归查询深度为 10 层 RelationManager.setMaxDepth(10); List<Menu> menus = menuMapper.selectListWithRelationsByQuery(qw);
RelationManager.setMaxDepth(10)
的配置,只在当前第一次查询有效,查询后会清除设置。
MyBatis-Flex 逻辑删除
假设在 tb_account 表中,存在一个为 is_deleted 的字段,用来标识该数据的逻辑删除,那么 tb_account 表 对应的 “Account.java” 实体类应该配置如下:
@Table("tb_account")public class Account {@Column(isLogicDelete = true)private Boolean isDelete;//Getter Setter...}@Table("tb_account") public class Account { @Column(isLogicDelete = true) private Boolean isDelete; //Getter Setter... }@Table("tb_account") public class Account { @Column(isLogicDelete = true) private Boolean isDelete; //Getter Setter... }
此时,当我们执行如下的删除代码是:
accountMapper.deleteById(1);accountMapper.deleteById(1);accountMapper.deleteById(1);
MyBatis 执行的 SQL 如下:
UPDATE `tb_account` SET `is_delete` = 1WHERE `id` = ? AND `is_delete` = 0UPDATE `tb_account` SET `is_delete` = 1 WHERE `id` = ? AND `is_delete` = 0UPDATE `tb_account` SET `is_delete` = 1 WHERE `id` = ? AND `is_delete` = 0
可以看出,当执行 deleteById 时,MyBatis 只是进行了 update 操作,而非 delete 操作。
注意事项
当 “tb_account” 的数据被删除时( is_delete = 1 时),我们通过 MyBatis-Flex 的 selectOneById 去查找数据时,会查询不到数据。 原因是 selectOneById
会自动添加上 is_delete = 0
条件,执行的 sql 如下:
SELECT * FROM tb_account where id = ? and is_delete = 0SELECT * FROM tb_account where id = ? and is_delete = 0SELECT * FROM tb_account where id = ? and is_delete = 0
不仅仅是 selectOneById 方法会添加 is_delete = 0
条件,BaseMapper 的以下方法也都会添加该条件:
- selectOneBy**
- selectListBy**
- selectCountBy**
- paginate
同时,比如 Left Join 或者子查询等,若 子表也设置了逻辑删除字段, 那么子表也会添加相应的逻辑删除条件,例如:
QueryWrapper query1 = QueryWrapper.create().select().from(ACCOUNT).leftJoin(ARTICLE).as("a").on(ACCOUNT.ID.eq(ARTICLE.ACCOUNT_ID)).where(ACCOUNT.AGE.ge(10));QueryWrapper query1 = QueryWrapper.create() .select() .from(ACCOUNT) .leftJoin(ARTICLE).as("a").on(ACCOUNT.ID.eq(ARTICLE.ACCOUNT_ID)) .where(ACCOUNT.AGE.ge(10));QueryWrapper query1 = QueryWrapper.create() .select() .from(ACCOUNT) .leftJoin(ARTICLE).as("a").on(ACCOUNT.ID.eq(ARTICLE.ACCOUNT_ID)) .where(ACCOUNT.AGE.ge(10));
其执行的 SQL 如下:
SELECT * FROM `tb_account`LEFT JOIN `tb_article` AS `a` ON `tb_account`.`id` = `a`.`account_id`WHERE `tb_account`.`age` >= 10AND `tb_account`.`is_delete` = 0 AND `a`.`is_delete` = 0SELECT * FROM `tb_account` LEFT JOIN `tb_article` AS `a` ON `tb_account`.`id` = `a`.`account_id` WHERE `tb_account`.`age` >= 10 AND `tb_account`.`is_delete` = 0 AND `a`.`is_delete` = 0SELECT * FROM `tb_account` LEFT JOIN `tb_article` AS `a` ON `tb_account`.`id` = `a`.`account_id` WHERE `tb_account`.`age` >= 10 AND `tb_account`.`is_delete` = 0 AND `a`.`is_delete` = 0
自动添加上 tb_account.is_delete = 0 AND a.is_delete = 0
条件。
示例 2:
QueryWrapper query2 = QueryWrapper.create().select().from(ACCOUNT).leftJoin(//子查询select().from(ARTICLE).where(ARTICLE.ID.ge(100))).as("a").on(ACCOUNT.ID.eq(raw("a.id"))).where(ACCOUNT.AGE.ge(10));QueryWrapper query2 = QueryWrapper.create() .select() .from(ACCOUNT) .leftJoin( //子查询 select().from(ARTICLE).where(ARTICLE.ID.ge(100)) ).as("a").on( ACCOUNT.ID.eq(raw("a.id")) ) .where(ACCOUNT.AGE.ge(10));QueryWrapper query2 = QueryWrapper.create() .select() .from(ACCOUNT) .leftJoin( //子查询 select().from(ARTICLE).where(ARTICLE.ID.ge(100)) ).as("a").on( ACCOUNT.ID.eq(raw("a.id")) ) .where(ACCOUNT.AGE.ge(10));
其执行的 SQL 如下:
SELECT * FROM `tb_account`LEFT JOIN (SELECT * FROM `tb_article` WHERE `id` >= 100 AND `is_delete` = 0) AS `a`ON `tb_account`.`id` = a.idWHERE `tb_account`.`age` >= 10 AND `tb_account`.`is_delete` = 0SELECT * FROM `tb_account` LEFT JOIN ( SELECT * FROM `tb_article` WHERE `id` >= 100 AND `is_delete` = 0 ) AS `a` ON `tb_account`.`id` = a.id WHERE `tb_account`.`age` >= 10 AND `tb_account`.`is_delete` = 0SELECT * FROM `tb_account` LEFT JOIN ( SELECT * FROM `tb_article` WHERE `id` >= 100 AND `is_delete` = 0 ) AS `a` ON `tb_account`.`id` = a.id WHERE `tb_account`.`age` >= 10 AND `tb_account`.`is_delete` = 0
数据脱敏
数据脱敏是什么
随着《网络安全法》的颁布施行,对个人隐私数据的保护已经上升到法律层面。 数据脱敏是指对某些敏感信息通过脱敏规则进行数据的变形, 实现敏感隐私数据的可靠保护。在涉及客户安全数据或者一些商业性敏感数据的情况下,在不违反系统规则条件下,对真实数据进行改造并提供使用, 如身份证号、手机号、卡号、客户号等个人信息都需要进行数据脱敏。
@ColumnMask
MyBatis-Flex 提供了 @ColumnMask()
注解,以及内置的 9 种脱敏规则,帮助开发者方便的进行数据脱敏。例如:
java
@Table("tb_account")public class Account {@Id(keyType = KeyType.Auto)private Long id;@ColumnMask(Masks.CHINESE_NAME)private String userName;}@Table("tb_account") public class Account { @Id(keyType = KeyType.Auto) private Long id; @ColumnMask(Masks.CHINESE_NAME) private String userName; }@Table("tb_account") public class Account { @Id(keyType = KeyType.Auto) private Long id; @ColumnMask(Masks.CHINESE_NAME) private String userName; }
以上的示例中,使用了 CHINESE_NAME
的脱敏规则,其主要用于处理 “中文名字” 的场景。当我们查询到 userName 为 张三丰
的时候,其内容自动被处理成 张**
。
除此之外,MyBatis-Flex 还提供了如下的 8 中脱敏规则(共9种),方便开发者直接使用:
- 手机号脱敏
- 固定电话脱敏
- 身份证号脱敏
- 车牌号脱敏
- 地址脱敏
- 邮件脱敏
- 密码脱敏
- 银行卡号脱敏
自定义脱敏规则
当 Mybaits-Flex 内置的 9 中脱敏规则无法满足要求时,我们还可以自定义脱敏规则,其步骤如下:
1、通过 MaskManager
注册新的脱敏规则:
MaskManager.registerMaskProcesser("自定义规则名称", data -> {return data;})MaskManager.registerMaskProcesser("自定义规则名称" , data -> { return data; })MaskManager.registerMaskProcesser("自定义规则名称" , data -> { return data; })
2、使用自定义的脱敏规则
@Table("tb_account")public class Account {@Id(keyType = KeyType.Auto)private Long id;@ColumnMask("自定义规则名称")private String userName;}@Table("tb_account") public class Account { @Id(keyType = KeyType.Auto) private Long id; @ColumnMask("自定义规则名称") private String userName; }@Table("tb_account") public class Account { @Id(keyType = KeyType.Auto) private Long id; @ColumnMask("自定义规则名称") private String userName; }
取消脱敏处理
在某些场景下,程序希望查询得到的数据是原始数据,而非脱敏数据。比如要去查询用户的手机号,然后给用户发送短信。又或者说,我们进入编辑页面编辑用户数据, 如果编辑页面展示的是脱敏数据,然后再次点击保存,那么数据库的真实数据也会被脱敏覆盖。
因此,MaskManager 提供了 execWithoutMask
、skipMask
、restoreMask
三个方法来处理这种场景:
推荐使用execWithoutMask
方法,该方法使用了模版方法设计模式,保障跳过脱敏处理并执行相关逻辑后自动恢复脱敏处理。
execWithoutMask
方法实现如下:
public static <T> T execWithoutMask(Supplier<T> supplier) {try {skipMask();return supplier.get();} finally {restoreMask();}}public static <T> T execWithoutMask(Supplier<T> supplier) { try { skipMask(); return supplier.get(); } finally { restoreMask(); } }public static <T> T execWithoutMask(Supplier<T> supplier) { try { skipMask(); return supplier.get(); } finally { restoreMask(); } }
使用方法:
AccountMapper mapper = ...;List<Account> accounts = MaskManager.execWithoutMask(mapper::selectAll);System.out.println(accounts);AccountMapper mapper = ...; List<Account> accounts = MaskManager.execWithoutMask(mapper::selectAll); System.out.println(accounts);AccountMapper mapper = ...; List<Account> accounts = MaskManager.execWithoutMask(mapper::selectAll); System.out.println(accounts);
skipMask
和restoreMask
方法需配套使用,推荐使用try{...}finally{...}
模式,如下例所示。 使用这两个方法可以自主控制跳过脱敏处理和恢复脱敏处理的时机。 当跳过脱敏处理和恢复脱敏处理无法放在同一个方法中时,可以使用这两个方法。 此时需要仔细处理代码分支及异常,以防止跳过脱敏处理后未恢复脱敏处理,导致安全隐患。
try {MaskManager.skipMask();//此处查询到的数据不会进行脱敏处理accountMapper.selectListByQuery(...);} finally {MaskManager.restoreMask();}try { MaskManager.skipMask(); //此处查询到的数据不会进行脱敏处理 accountMapper.selectListByQuery(...); } finally { MaskManager.restoreMask(); }try { MaskManager.skipMask(); //此处查询到的数据不会进行脱敏处理 accountMapper.selectListByQuery(...); } finally { MaskManager.restoreMask(); }
SQL 审计
SQL 审计是一项非常重要的工作,是企业数据安全体系的重要组成部分,通过 SQL 审计功能为数据库请求进行全程记录,为事后追溯溯源提供了一手的信息,同时可以通过可以对恶意访问及时警告管理员,为防护策略优化提供数据支撑。
同时、提供 SQL 访问日志长期存储,满足等保合规要求。
开启审计功能^1.0.5
Mybaits-Flex 的 SQL 审计功能,默认是关闭的,若开启审计功能,许添加如下配置。
AuditManager.setAuditEnable(true)AuditManager.setAuditEnable(true)AuditManager.setAuditEnable(true)
默认情况下,Mybaits-Flex 的审计消息(日志)只会输出到控制台,如下所示:
>>>>>>Sql Audit: {platform='mybatis-flex', module='null', url='null', user='null', userIp='null', hostIp='192.168.3.24', query='SELECT * FROM `tb_account` WHERE `id` = ?', queryParams=[1], queryTime=1679991024523, elapsedTime=1}>>>>>>Sql Audit: {platform='mybatis-flex', module='null', url='null', user='null', userIp='null', hostIp='192.168.3.24', query='SELECT * FROM `tb_account` WHERE `id` = ?', queryParams=[1], queryTime=1679991024854, elapsedTime=3}>>>>>>Sql Audit: {platform='mybatis-flex', module='null', url='null', user='null', userIp='null', hostIp='192.168.3.24', query='SELECT * FROM `tb_account` WHERE `id` = ?', queryParams=[1], queryTime=1679991025100, elapsedTime=2}>>>>>>Sql Audit: {platform='mybatis-flex', module='null', url='null', user='null', userIp='null', hostIp='192.168.3.24', query='SELECT * FROM `tb_account` WHERE `id` = ?', queryParams=[1], queryTime=1679991024523, elapsedTime=1} >>>>>>Sql Audit: {platform='mybatis-flex', module='null', url='null', user='null', userIp='null', hostIp='192.168.3.24', query='SELECT * FROM `tb_account` WHERE `id` = ?', queryParams=[1], queryTime=1679991024854, elapsedTime=3} >>>>>>Sql Audit: {platform='mybatis-flex', module='null', url='null', user='null', userIp='null', hostIp='192.168.3.24', query='SELECT * FROM `tb_account` WHERE `id` = ?', queryParams=[1], queryTime=1679991025100, elapsedTime=2}>>>>>>Sql Audit: {platform='mybatis-flex', module='null', url='null', user='null', userIp='null', hostIp='192.168.3.24', query='SELECT * FROM `tb_account` WHERE `id` = ?', queryParams=[1], queryTime=1679991024523, elapsedTime=1} >>>>>>Sql Audit: {platform='mybatis-flex', module='null', url='null', user='null', userIp='null', hostIp='192.168.3.24', query='SELECT * FROM `tb_account` WHERE `id` = ?', queryParams=[1], queryTime=1679991024854, elapsedTime=3} >>>>>>Sql Audit: {platform='mybatis-flex', module='null', url='null', user='null', userIp='null', hostIp='192.168.3.24', query='SELECT * FROM `tb_account` WHERE `id` = ?', queryParams=[1], queryTime=1679991025100, elapsedTime=2}
Mybaits-Flex 消息包含了如下内容:
- platform:平台,或者是运行的应用
- module:应用模块
- url:执行这个 SQL 涉及的 URL 地址
- user:执行这个 SQL 涉及的 平台用户
- userIp:执行这个 SQL 的平台用户 IP 地址
- hostIp:执行这个 SQL 的服务器 IP 地址
- query:SQL 内容
- queryParams:SQL 参数
- queryTime:SQL 执行的时间点(当前时间)
- elapsedTime:SQL 执行的消耗时间(毫秒)
- metas:其他扩展元信息
事务管理
MyBatis-Flex 提供了一个名为 Db.tx()
的方法^1.0.6,用于进行事务管理,若使用 Spring 框架的场景下,也可使用 @Transactional
注解进行事务管理。
Db.tx()
方法定义如下:
boolean tx(Supplier<Boolean> supplier);boolean tx(Supplier<Boolean> supplier, Propagation propagation);<T> T txWithResult(Supplier<T> supplier);<T> T txWithResult(Supplier<T> supplier, Propagation propagation);boolean tx(Supplier<Boolean> supplier); boolean tx(Supplier<Boolean> supplier, Propagation propagation); <T> T txWithResult(Supplier<T> supplier); <T> T txWithResult(Supplier<T> supplier, Propagation propagation);boolean tx(Supplier<Boolean> supplier); boolean tx(Supplier<Boolean> supplier, Propagation propagation); <T> T txWithResult(Supplier<T> supplier); <T> T txWithResult(Supplier<T> supplier, Propagation propagation);
方法:
- tx:返回结果为 Boolean,返回
null
或者false
或者 抛出异常,事务回滚 - txWithResult:返回结果由
Supplier
参数决定,只有抛出异常时,事务回滚
参数:
- supplier:要执行的内容(代码)
- propagation:事务传播属性
事务传播属性 propagation
是一个枚举类,其枚举内容如下:
//若存在当前事务,则加入当前事务,若不存在当前事务,则创建新的事务REQUIRED(0),//若存在当前事务,则加入当前事务,若不存在当前事务,则已非事务的方式运行SUPPORTS(1),//若存在当前事务,则加入当前事务,若不存在当前事务,则抛出异常MANDATORY(2),//始终以新事务的方式运行,若存在当前事务,则暂停(挂起)当前事务。REQUIRES_NEW(3),//以非事务的方式运行,若存在当前事务,则暂停(挂起)当前事务。NOT_SUPPORTED(4),//以非事务的方式运行,若存在当前事务,则抛出异常。NEVER(5),//暂时不支持NESTED(6),//若存在当前事务,则加入当前事务,若不存在当前事务,则创建新的事务 REQUIRED(0), //若存在当前事务,则加入当前事务,若不存在当前事务,则已非事务的方式运行 SUPPORTS(1), //若存在当前事务,则加入当前事务,若不存在当前事务,则抛出异常 MANDATORY(2), //始终以新事务的方式运行,若存在当前事务,则暂停(挂起)当前事务。 REQUIRES_NEW(3), //以非事务的方式运行,若存在当前事务,则暂停(挂起)当前事务。 NOT_SUPPORTED(4), //以非事务的方式运行,若存在当前事务,则抛出异常。 NEVER(5), //暂时不支持 NESTED(6),//若存在当前事务,则加入当前事务,若不存在当前事务,则创建新的事务 REQUIRED(0), //若存在当前事务,则加入当前事务,若不存在当前事务,则已非事务的方式运行 SUPPORTS(1), //若存在当前事务,则加入当前事务,若不存在当前事务,则抛出异常 MANDATORY(2), //始终以新事务的方式运行,若存在当前事务,则暂停(挂起)当前事务。 REQUIRES_NEW(3), //以非事务的方式运行,若存在当前事务,则暂停(挂起)当前事务。 NOT_SUPPORTED(4), //以非事务的方式运行,若存在当前事务,则抛出异常。 NEVER(5), //暂时不支持 NESTED(6),
Db.tx()
代码示例:
Db.tx(() -> {//进行事务操作return true;});Db.tx(() -> { //进行事务操作 return true; });Db.tx(() -> { //进行事务操作 return true; });
若 tx()
方法抛出异常,或者返回 false,或者返回 null,则回滚事务。只有正常返回 true 的时候,进行事务提交。
嵌套事务
示例代码:
Db.tx(() -> {//进行事务操作boolean success = Db.tx(() -> {//另一个事务的操作return true;});return true;});Db.tx(() -> { //进行事务操作 boolean success = Db.tx(() -> { //另一个事务的操作 return true; }); return true; });Db.tx(() -> { //进行事务操作 boolean success = Db.tx(() -> { //另一个事务的操作 return true; }); return true; });
支持无限极嵌套,默认情况下,嵌套事务直接的关系是:REQUIRED
(若存在当前事务,则加入当前事务,若不存在当前事务,则创建新的事务)。
@Transactional
MyBatis-Flex 已支持 Spring 框架的 @Transactional
,在使用 SpringBoot 的情况下,可以直接使用 @Transactional
进行事务管理。 同理,使用 Spring 的 TransactionTemplate
进行事务管理也是没问题的。
注意:若项目未使用 SpringBoot,只用到了 Spring,需要参考 MyBatis-Flex 的 FlexTransactionAutoConfiguration 进行事务配置,才能正常使用
@Transactional
注解。
特征
- 1、支持嵌套事务
- 2、支持多数据源
注意:在多数据源的情况下,所有数据源的数据库请求(Connection)会执行相同的 commit 或者 rollback,但并非原子操作。例如:
@Transactionalpublic void doSomething(){try{DataSourceKey.use("ds1");Db.updateBySql("update ....");}finally{DataSourceKey.clear()}try{DataSourceKey.use("ds2");Db.updateBySql("update ...");}finally{DataSourceKey.clear()}//抛出异常int x = 1/0;}@Transactional public void doSomething(){ try{ DataSourceKey.use("ds1"); Db.updateBySql("update ...."); }finally{ DataSourceKey.clear() } try{ DataSourceKey.use("ds2"); Db.updateBySql("update ..."); }finally{ DataSourceKey.clear() } //抛出异常 int x = 1/0; }@Transactional public void doSomething(){ try{ DataSourceKey.use("ds1"); Db.updateBySql("update ...."); }finally{ DataSourceKey.clear() } try{ DataSourceKey.use("ds2"); Db.updateBySql("update ..."); }finally{ DataSourceKey.clear() } //抛出异常 int x = 1/0; }
在以上的例子中,两次 Db.update(...)
虽然是两个不同的数据源,但它们都在同一个事务 @Transactional
里,因此,当抛出异常的时候, 它们都会进行回滚(rollback)。
以上提到的 并非原子操作
,指的是:
假设在回滚的时候,恰好其中一个数据库出现了异常(比如 网络问题,数据库崩溃),此时,可能只有一个数据库的数据正常回滚(rollback)。 但无论如何,MyBatis-Flex 都会保证在同一个
@Transactional
中的多个数据源,保持相同的 commit 或者 rollback 行为。
字段权限
字段权限,指的是在一张表中设计了许多字段,但是不同的用户(或者角色)查询,返回的字段结果是不一致的。 比如:tb_account 表中,有 user_name 和 password 字段,但是 password 字段只允许用户本人查询, 或者超级管理员查询,这种场景下,我们会用到 字段权限 的功能。
在 @Table()
注解中,有一个配置名为 onSet
,用于设置这张表的 设置
监听,这里的 设置
监听指的是: 当我们使用 sql 、调用某个方法去查询数据,得到的数据内容映射到 entity 实体,mybatis 通过 setter 方法去设置 entity 的值时的监听。
以下是示例:
step 1: 为实体类编写一个 set 监听器(SetListener
)
public class AccountOnSetListener implements SetListener {@Overridepublic Object onSet(Object entity, String property, Object value) {if (property.equals("password")){//去查询当前用户的权限boolean hasPasswordPermission = getPermission();//若没有权限,则把数据库查询到的 password 内容修改为 nullif (!hasPasswordPermission){value = null;}}return value;}}public class AccountOnSetListener implements SetListener { @Override public Object onSet(Object entity, String property, Object value) { if (property.equals("password")){ //去查询当前用户的权限 boolean hasPasswordPermission = getPermission(); //若没有权限,则把数据库查询到的 password 内容修改为 null if (!hasPasswordPermission){ value = null; } } return value; } }public class AccountOnSetListener implements SetListener { @Override public Object onSet(Object entity, String property, Object value) { if (property.equals("password")){ //去查询当前用户的权限 boolean hasPasswordPermission = getPermission(); //若没有权限,则把数据库查询到的 password 内容修改为 null if (!hasPasswordPermission){ value = null; } } return value; } }
step 2: 为实体类配置 onSet
监听
@Table(value = "tb_account", onSet = AccountOnSetListener.class)public class Account {@Id(keyType = KeyType.Auto)private Long id;private String userName;private String password;//getter setter}@Table(value = "tb_account", onSet = AccountOnSetListener.class) public class Account { @Id(keyType = KeyType.Auto) private Long id; private String userName; private String password; //getter setter }@Table(value = "tb_account", onSet = AccountOnSetListener.class) public class Account { @Id(keyType = KeyType.Auto) private Long id; private String userName; private String password; //getter setter }
字段加密
字段加密,指的是数据库在存入了明文内容,但是当我们进行查询时,返回的内容为加密内容,而非明文内容。
以下是 MyBatis-Flex 字段加密示例:
step 1: 为实体类编写一个 set 监听器(SetListener
)
public class AccountOnSetListener implements SetListener {@Overridepublic Object onSet(Object entity, String property, Object value) {if (value != null){//对字段内容进行加密value = encrypt(value);}return value;}}public class AccountOnSetListener implements SetListener { @Override public Object onSet(Object entity, String property, Object value) { if (value != null){ //对字段内容进行加密 value = encrypt(value); } return value; } }public class AccountOnSetListener implements SetListener { @Override public Object onSet(Object entity, String property, Object value) { if (value != null){ //对字段内容进行加密 value = encrypt(value); } return value; } }
step 2: 为实体类配置 onSet
监听
@Table(value = "tb_account", onSet = AccountOnSetListener.class)public class Account {@Id(keyType = KeyType.Auto)private Long id;private String userName;private String password;//getter setter}@Table(value = "tb_account", onSet = AccountOnSetListener.class) public class Account { @Id(keyType = KeyType.Auto) private Long id; private String userName; private String password; //getter setter }@Table(value = "tb_account", onSet = AccountOnSetListener.class) public class Account { @Id(keyType = KeyType.Auto) private Long id; private String userName; private String password; //getter setter }