大数跨境
0
0

增强版 MyBatis-Flex 优雅魅力,来体验下!

增强版 MyBatis-Flex 优雅魅力,来体验下! 架构师专栏
2025-10-24
8

大家好,我是鹏磊。


https://www.ddkk.com


前言

MyBatis-Flex 是一个优雅的 MyBatis 增强框架,它非常轻量、同时拥有极高的性能与灵活性。我们可以轻松的使用 Mybaits-Flex 链接任何数据库,其内置的 QueryWrapper亮点 帮助我们极大的减少了 SQL 编写的工作的同时,减少出错的可能性。

更轻量

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+ 倍。

官网:https://mybatis-flex.com/

代码实践

除了Mybatis-plus带的那些功能,Mybatis-Flex提供了多主键、复合主键功能;提供了关联查询;特别是关联查询在日常业务开发碰到的场景很多。

Mybatis-Flex提供了一对一、一对多、多对一、多对多的场景。

1.添加依赖

<dependency>
    <groupId>com.mybatis-flex</groupId>
    <artifactId>mybatis-flex-spring-boot-starter</artifactId>
    <version>1.5.3</version>
</dependency>
<dependency>
    <groupId>com.mybatis-flex</groupId>
    <artifactId>mybatis-flex-processor</artifactId>
    <version>1.5.3</version>
</dependency>
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <version>8.0.31</version>
</dependency>
<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

一对一关联查询 @RelationOneToOne

假设有一个账户,账户有身份证,账户和身份证的关系是一对一的关系,代码如下所示:

@Data
publicclassAccountimplementsSerializable {

    @Id(keyType = KeyType.Auto)
    private Long id;

    private String userName;

    @RelationOneToOne(selfField = "id", targetField = "accountId")
    private IDCard idCard;
}

@Data
@Table(value = "tb_idcard")
publicclassIDCardimplementsSerializable {

    private Long accountId;
    private String cardNo;
    private String content;
}

若 selfField 是主键,且当前表只有 1 个主键时,可以不填写。因此,以上的配置可以简化为 @RelationOneToOne(targetField = "accountId")

执行sql:

SELECT `id`, `user_name`, `age` FROM `tb_account`

SELECT `account_id`, `card_no`, `content` FROM `tb_idcard`
WHERE account_id IN (12345)

结果打印:

[
 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

假设一个账户有很多本书籍,一本书只能归属一个账户所有;账户和书籍的关系是一对多的关系,代码如下:

@Data
publicclassAccountimplementsSerializable {

    @Id(keyType = KeyType.Auto)
    private Long id;

    private String userName;

    @RelationOneToMany(selfField = "id", targetField = "accountId")
    private List<Book> books;
}

@Data
@Table(value = "tb_book")
publicclassBookimplementsSerializable {

    @Id(keyType = KeyType.Auto)
    private Long id;
    private Long accountId;
    private String title;
}

若 Account.books 是一个 Map,而非 List,那么,我们需要通过配置 mapKeyField 来指定列来充当 Map 的 Key, 如下代码所示:

@Data
publicclassAccountimplementsSerializable {

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

多对一关联查询 @RelationManyToOne

假设一个账户有很多本书籍,一本书只能归属一个账户所有;账户和书籍的关系是一对多的关系,书籍和账户的关系为多对一的关系,代码如下:

@Data
publicclassAccountimplementsSerializable {

    @Id(keyType = KeyType.Auto)
    private Long id;

    private String userName;
}

@Data
@Table(value = "tb_book")
publicclassBookimplementsSerializable {

    @Id(keyType = KeyType.Auto)
    private Long id;
    private Long accountId;
    private String title;

    @RelationManyToOne(selfField = "accountId", targetField = "id")
    private Account account;
}

多对多关联查询 @RelationManyToMany

假设一个账户可以有多个角色,一个角色也可以有多个账户,他们是多对多的关系,需要通过中间表 tb_role_mapping 来维护:

@Data
publicclassAccountimplementsSerializable {

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

@Data
@Table(value = "tb_role")
publicclassRoleimplementsSerializable {

    private Long id;
    private String name;
}

父子关系查询

@Data
@Table(value = "tb_menu")
publicclassMenuimplementsSerializable {

    private Long id;

    private Long parentId;

    private String name;

    @RelationOneToMany(selfField = "id", targetField = "parentId")
    private List<Menu> children;
}

在以上的父子关系查询中,默认的递归查询深度为 3 个层级,若需要查询指定递归深度,需要添加如下配置:

QueryWrapper qw = QueryWrapper.create();
qw.where(MENU.PARENT_ID.eq(0));

//设置递归查询深度为 10 层
RelationManager.setMaxDepth(10);
List<Menu> menus = menuMapper.selectListWithRelationsByQuery(qw);

链式操作

在 MyBatis-Flex 中,内置了 QueryChain.java 、 UpdateChain.java 以及 DbChain.java 用于对数据进行链式查询操作和链式操作(修改和删除)。

例如,查询文章列表代码如下:

@SpringBootTest
classArticleServiceTest {

    @Autowired
    ArticleService articleService;

    @Test
    voidtestChain() {
        List<Article> articles = articleService.queryChain()
            .select(ARTICLE.ALL_COLUMNS)
            .from(ARTICLE)
            .where(ARTICLE.ID.ge(100))
            .list();
    }
}

若不是在 Service 中,我们也可以通过 QueryChain.of(mapper) 方法,自己创建一个 QueryChain 实例,代码如下:

List<Article> articles = QueryChain.of(mapper)
    .select(ARTICLE.ALL_COLUMNS)
    .from(ARTICLE)
    .where(ARTICLE.ID.ge(100))
    .list();

数据脱敏

对真实数据进行改造并提供使用, 如身份证号、手机号、卡号、客户号等个人信息都需要进行数据脱敏``MyBatis-Flex提供了@ColumnMask() 注解,以及内置的9种脱敏规则,帮助开发者方便的进行数据脱敏。例如:

@Table("tb_account")
public class Account {

    @Id(keyType = KeyType.Auto)
    private Long id;

    @ColumnMask(Masks.CHINESE_NAME)
    private String userName;
}

除此之外,MyBatis-Flex 还提供了如下的8种脱敏规则,方便开发者直接使用:

  • 手机号脱敏
  • 固定电话脱敏
  • 身份证号脱敏
  • 车牌号脱敏
  • 地址脱敏
  • 邮件脱敏
  • 密码脱敏
  • 银行卡号脱敏

当 Mybaits-Flex 内置的9种脱敏规则无法满足要求时,我们还可以自定义脱敏规则,其步骤如下:

MaskManager.registerMaskProcessor("自定义规则名称"
        , data -> {
            return data;
        })

在某些场景下,程序希望查询得到的数据是原始数据,而非脱敏数据。比如要去查询用户的手机号,然后给用户发送短信。又或者说,我们进入编辑页面编辑用户数据, 如果编辑页面展示的是脱敏数据,然后再次点击保存,那么数据库的真实数据也会被脱敏覆盖。

因此,MaskManager 提供了 execWithoutMask、skipMask、restoreMask 三个方法来处理这种场景:

try {
    MaskManager.skipMask();

    //此处查询到的数据不会进行脱敏处理
    accountMapper.selectListByQuery(...);
finally {
    MaskManager.restoreMask();
}

数据缓存

MyBatis-Flex 是一个 MyBatis 增强框架,所以您可以使用 MyBatis 提供的二级缓存来作为数据缓存。但是它仍然有很多的缺点,比如不适用于分布式环境,在这里推荐使用 Spring Cache 模块来处理数据缓存。

@Service
@CacheConfig(cacheNames = "account")
publicclassAccountServiceImplextendsCacheableServiceImpl<MyAccountMapper, Account> {

    @Override
    @CacheEvict(allEntries = true)
    publicbooleanremove(QueryWrapper query) {
        returnsuper.remove(query);
    }

    @Override
    @CacheEvict(key = "#id")
    publicbooleanremoveById(Serializable id) {
        returnsuper.removeById(id);
    }

    @Override
    @CacheEvict(allEntries = true)
    publicbooleanremoveByIds(Collection<? extends Serializable> ids) {
        returnsuper.removeByIds(ids);
    }

    // 根据查询条件更新时,实体类主键可能为 null。
    @Override
    @CacheEvict(allEntries = true)
    publicbooleanupdate(Account entity, QueryWrapper query) {
        returnsuper.update(entity, query);
    }

    @Override
    @CacheEvict(key = "#entity.id")
    publicbooleanupdateById(Account entity, boolean ignoreNulls) {
        returnsuper.updateById(entity, ignoreNulls);
    }

    @Override
    @CacheEvict(allEntries = true)
    publicbooleanupdateBatch(Collection<Account> entities, int batchSize) {
        returnsuper.updateBatch(entities, batchSize);
    }

    @Override
    @Cacheable(key = "#id")
    public Account getById(Serializable id) {
        returnsuper.getById(id);
    }

    @Override
    @Cacheable(key = "#root.methodName + ':' + #query.toSQL()")
    public Account getOne(QueryWrapper query) {
        returnsuper.getOne(query);
    }

    @Override
    @Cacheable(key = "#root.methodName + ':' + #query.toSQL()")
    public <R> R getOneAs(QueryWrapper query, Class<R> asType) {
        returnsuper.getOneAs(query, asType);
    }

    @Override
    @Cacheable(key = "#root.methodName + ':' + #query.toSQL()")
    public List<Account> list(QueryWrapper query) {
        returnsuper.list(query);
    }

    @Override
    @Cacheable(key = "#root.methodName + ':' + #query.toSQL()")
    public <R> List<R> listAs(QueryWrapper query, Class<R> asType) {
        returnsuper.listAs(query, asType);
    }

    // 无法通过注解进行缓存操作
    @Override
    @Deprecated
    public List<Account> listByIds(Collection<? extends Serializable> ids) {
        returnsuper.listByIds(ids);
    }

    @Override
    @Cacheable(key = "#root.methodName + ':' + #query.toSQL()")
    publiclongcount(QueryWrapper query) {
        returnsuper.count(query);
    }

    @Override
    @Cacheable(key = "#root.methodName + ':' + #page.getPageSize() + ':' + #page.getPageNumber() + ':' + #query.toSQL()")
    public <R> Page<R> pageAs(Page<R> page, QueryWrapper query, Class<R> asType) {
        returnsuper.pageAs(page, query, asType);
    }

}

SQL审计

Mybaits-Flex 的 SQL 审计功能,默认是关闭的,若开启审计功能,需添加如下配置。

如果你近期准备面试跳槽,建议在ddkk.com在线刷题,涵盖 一万+ 道 Java 面试题,几乎覆盖了所有主流技术面试题,还有市面上最全的技术五百套,精品系列教程,免费提供。


AuditManager.setAuditEnable(true)

MyBatis-Flex 内置了一个名为 MessageFactory 的接口,我们只需实现该接口,并为 AuditManager 配置新的 MessageFactory 即可,如下所示:

public classMyMessageFactoryimplementsMessageFactory {

    @Override
    public AuditMessage create() {
        AuditMessagemessage=newAuditMessage();

        // 在这里
        // 设置 message 的基础内容,包括 platform、module、url、user、userIp、hostIp 内容
        // 剩下的 query、queryParams、queryCount、queryTime、elapsedTime 为 mybatis-flex 设置

        return message;
    }
}

并为 AuditManager 配置新写的 MyMessageFactory

MessageFactory creator = new MyMessageFactory();
AuditManager.setMessageFactory(creator);

自定义 MessageReporter:

public classMyMessageReporterimplementsMessageReporter {

    @Override
    publicvoidsendMessages(List<AuditMessage> messages) {
        //在这里把 messages 审计日志发送到指定位置
        //比如 
        // 1、通过 http 协议发送到指定服务器
        // 2、通过日志工具发送到日志平台
        // 3、通过 Kafka 等 MQ 发送到指定平台
    }

}

自定义 MessageCollector:

public class MyMessageCollector implements MessageCollector {
    @Override
    public void collect(AuditMessage auditMessage) {
        System.out.println(auditMessage.getFullSql());
    }
}

MyBatis-Flex 内置了两个 Collector,他们分别是:

  • ScheduledMessageCollector
     定时把消息通过 MessageReporter 发送到指定位置。
  • ConsoleMessageCollector
     使用其把消息输出到控制台。

多数据源

MyBaits-Flex 内置了功能完善的多数据源支持,不需要借助第三方插件或者依赖,开箱即用, 支持包括 druid、hikaricp、dbcp2、beecp 在内的任何数据源,MyBatis-Flex 多数据源配置如下:

mybatis-flex:
  datasource:
    ds1:
      url: jdbc:mysql://127.0.0.1:3306/db
      username: root
      password: 123456
    ds2:
      url: jdbc:mysql://127.0.0.1:3306/db2
      username: root
      password: 123456

MyBatis-Flex 提供了 4 种方式来配置数据源:

  • 编码,使用DataSourceKey.use 方法。
  • @UseDataSource("dataSourceName")
     在 Mapper 类上,添加注解,用于指定使用哪个数据源。
  • @UseDataSource("dataSourceName")
     在 Mapper 方法上,添加注解,用于指定使用哪个数据源。
  • @Table(dataSource="dataSourceName")
     在 Entity 类上添加注解,该 Entity 的增删改查请求默认使用该数据源。

优先级:DataSourceKey.use() > @UseDataSource()在方法上 > @UseDataSource()在类上 >@Table(dataSource="...")

try{
    DataSourceKey.use("ds2")
    List<Row> rows = Db.selectAll("tb_account");
    System.out.println(rows);
}finally{
    DataSourceKey.clear();
}

整体来讲,这个框架是Mybatis的增强版,几乎集成了mybatis plus、jooq、fluent mybatis的所有优点,更多的大家可以探索一番。

https://www.ddkk.com

如果你近期准备面试跳槽,建议在ddkk.com在线刷题,涵盖 一万+ 道 Java 面试题,几乎覆盖了所有主流技术面试题,还有市面上最全的技术五百套,精品系列教程,免费提供。

【声明】内容源于网络
0
0
架构师专栏
专注原创,架构设计,Java后端、微服务、算法、技术栈。资料网站 ddkk.com
内容 1254
粉丝 0
架构师专栏 专注原创,架构设计,Java后端、微服务、算法、技术栈。资料网站 ddkk.com
总阅读1.7k
粉丝0
内容1.3k