Let's Fluent:更顺滑的MyBatis

简介: 只需瞅一眼Google Trends上全球Java界最热门的两款SQL映射框架近一年的对比数字,就不难了解其实力分布:在此领域,MyBatis早已占领东亚地区开发者市场,并以绝对优势稳居中国最抢手Java数据库访问框架之首。

image.png

作者 | 金戟
来源 | 阿里技术公众号

只需瞅一眼Google Trends上全球Java界最热门的两款SQL映射框架近一年的对比数字,就不难了解其实力分布:在此领域,MyBatis早已占领东亚地区开发者市场,并以绝对优势稳居中国最抢手Java数据库访问框架之首。

MyBatis霸榜的底气来源于其广袤的生态以及国内众多大厂的支持。而在琳琅满目的MyBatis扩展中,还埋藏着许多“宝藏项目”,来自阿里技术团队的Fluent MyBatis便是其中一颗独特的新星。

一 普拉斯们不香了

从iBatis到MyBatis,再到国内团队以MyBatis Plus为典型代表的诸多周边工具,"Batis"系列套餐的发展历程,几乎又是一部XML的兴衰史。最初的iBatis诞生于2002年,彼时XML在Java乃至整个软件技术界都还相当盛行,和同时期的许多项目一样,iBatis硬生生的将一堆堆XML塞进千家万户的项目里。

许多年后,曾今与iBatis并肩过的社区战友们纷纷淡出了历史舞台,少数像Spring这样延续至今的佼佼者,也逐渐摒弃XML,向代码化配置的方式发展。在这方面,iBatis一直是个保守派,即使在MyBatis接过iBatis的衣钵之后,也只是”重磅“推出了支持代码执行SQL的@Select/@Insert/@Update/@Delete注解(以及相应的4种Provider注解),用来抵挡开发者们对XML泛滥的吐槽,这是在2010年中旬,然后就再无动作。直到2016年底,MyBatis的主要贡献者之一Jeff Butler正式创建MyBatis Dynamic SQL项目,MyBatis终于开始全面拥抱无XML的代码化SQL构建。

在从MyBatis到MyBatis Dynamic SQL之间长达6年多的空窗期里,开源社区催生出了许多民间基于MyBatis的无XML代码方案,其中流行得比较广泛的是Tk Mybatis、MyBatis Plus这类内置Mapper和自动生成CRUD的扩展库,一经推出就收获诸多好评。包括MyBatis Plus里实际上并不太完备的"条件构造器"功能,也由于当时同类解决方案的匮乏而颇受追捧。与此同时,在MyBatis社区之外,一直在默默发展的JOOQ是一款历史与MyBatis几乎同样悠久的纯Java动态SQL执行库,它的用户群体不大,却口碑甚好。如今在任意搜索引擎上输入"MyBatis vs JOOQ",依然能得到几乎是一边倒选择JOOQ的结果,大家给出的理由也非常一致:简洁、灵活、无需XML,很"Java"。而在MyBatis阵营里,若是拿出MyBatis Plus的"条件构造器"与之正面对阵,只消三个回合,就会被屁滚尿流的打出擂台。只可惜JOOQ的家底没有MyBatis那样殷实,早早走上了商业数据库支持卖License收费的道路,才让MyBatis免于在舆论上迎来自己的中年危机。

Fluent MyBatis诞生于2019年底,即使与MyBatis Dynamic SQL相比都是晚辈,然而尚处成长期的它就已透出了青出于蓝而胜于蓝的味道。

在实现方式上,MyBatis Plus覆写并替换了部分MyBatis内部类型的方法,整体机制较重,却也因此能将一些功能细节隐藏到用户无需关注的内部逻辑里;与之相反,MyBatis Dynamic SQL的实现机制非常轻量,不仅完全基于MyBatis原生的Provider系列注解开发,而且没有什么隐藏逻辑,对用户的每张表自动生成相应的Entity、DynamicSqlSupport和Mapper三个类,全部放入用户的源码目录里,因此暴露的细节比较多,代码侵入性略高。Fluent MyBatis取二者之所长,整体机制与MyBatis Dynamic SQL更接近,同样基于原生的Provider注解,对用户的每个表生成Entity类和默认空白的Dao类,不同之处在于它还会通过JVM编译期代码增强功能自动生成许多开发者不可更改的标准辅助类,这些代码无需放入用户的源码目录但能够在编码时直接使用,即提供丰富的功能,又保证了用户代码的整洁。

在使用方式上,Fluent MyBatis同样借鉴了前辈们的最优实践,没有花里胡哨的注解和配置,直接复用MyBatis连接,所有功能开箱即用。同时由于Fluent MyBatis将所有表字段、条件、操作都以方法调用形式提供,因此获得了比其他同类项目都更好的IDE语法辅助。举一个不太复杂的例子:

// 使用Fluent MyBatis构造查询语句
mapper.listMaps(new StudentScoreQuery()
    .select
    .schoolTerm()
    .subject()
    .count.score("count")
    .min.score("min_score")
    .max.score("max_score")
    .avg.score("avg_score")
    .end()
    .where.schoolTerm().ge(2000)
    .and.subject.in(new String[]{"英语", "数学", "语文"})
    .and.score().ge(60)
    .and.isDeleted().isFalse()
    .end()
    .groupBy.schoolTerm().subject().end()
    .having.count.score.gt(1).end()
    .orderBy.schoolTerm().asc().subject().asc().end()
);

MyBatis Dynamic SQL的语法也比较美观,但字段名和min/max/avg等方法都需要静态引用,比Fluent MyBatis稍显逊色。

// 使用MyBatis Dynamic SQL构造查询语句
mapper.selectMany(
    select(
        schoolTerm,
        subject,
        count(score).as("count"),
        min(score).as("min_score"),
        max(score).as("max_score"),
        avg(score).as("avg_score")
    ).from(studentScore)
    .where(schoolTerm, isGreaterThanOrEqualTo(2000))
    .and(subject, isIn("英语", "数学", "语文"))
    .and(score, isGreaterThanOrEqualTo(60))
    .and(isDeleted, isEqualTo(false))
    .groupBy(schoolTerm, subject)
    .having(count(score), isGreaterThan(1)) //当前其实还不支持having方法
    .orderBy(schoolTerm, subject)
    .build(isDeleted, isEqualTo(false))
    .render(RenderingStrategies.MYBATIS3)
);

JOOQ的历史比较悠久,写出来的代码铺天盖地都是常量字段,功能强大但美观度欠佳。

// 使用JOOQ构造查询语句
dslContext.select(
    STUDENT_SCORE.GENDER_MAN,
    STUDENT_SCORE.SCHOOL_TERM,
    STUDENT_SCORE.SUBJECT,
    count(STUDENT_SCORE.SCORE).as("count"),
    min(STUDENT_SCORE.SCORE).as("min_score"),
    max(STUDENT_SCORE.SCORE).as("max_score"),
    avg(STUDENT_SCORE.SCORE).as("avg_score")
)
.from(STUDENT_SCORE)
.where(
    STUDENT_SCORE.SCHOOL_TERM.ge(2000),
    STUDENT_SCORE.SUBJECT.in("英语", "数学", "语文"),
    STUDENT_SCORE.SCORE.ge(60),
    STUDENT_SCORE.IS_DELETED.eq(false)
)
.groupBy(
    STUDENT_SCORE.GENDER_MAN,
    STUDENT_SCORE.SCHOOL_TERM,
    STUDENT_SCORE.SUBJECT
)
.having(count().ge(1))
.orderBy(
    STUDENT_SCORE.SCHOOL_TERM.asc(),
    STUDENT_SCORE.SUBJECT.asc()
)
.fetch();

MyBatis Plus的条件构造器仅仅封装了基本的SQL操作,对于字段、条件、别名等都要使用字符串拼接,极易出现由于拼写失误引起的SQL异常。

// 使用MyBatis Plus构造查询语句
mapper.selectMaps(new QueryWrapper<StudentScore>()
    .select(
        "school_term",
        "subject",
        "count(score) as count",
        "min(score) as min_score",
        "max(score) as max_score",
        "avg(score) as avg_score"
    )
    .ge("school_term", 2000)
    .in("subject", "英语", "数学", "语文")
    .ge("score", 60)
    .eq("is_deleted", false)
    .groupBy("school_term", "subject")
    .having("count(score)>1")
    .orderByAsc("school_term", "subject")
);

在Java动态SQL构建的功能完整度方面,当前的排序是MyBatis Plus < MyBatis Dynamic SQL < Fluent MyBatis < JOOQ。

MyBatis Plus条件构造器在功能性上完败,不仅无法表达JOIN、UNION语句,嵌套查询之类稍复杂SQL也完全没招。MyBatis Dynamic SQL支持JOIN和UNION语句,尚未支持嵌套查询,且缺少HAVING等少量标准SQL语法。Fluent MyBatis支持多表JOIN、UNION、嵌套查询和几乎所有标准SQL语法,对于绝大多数场景都妥妥够用。JOOQ是真正的王者,不仅支持标准SQL语法,连各厂商特有的专有关键字和内置方法都没放过,如MySQL的ON DUPLICATE KEY UPDATE、PostgreSQL的WINDOW、Oracle的CONNECT BY等等。补齐各种SQL语法是一件琐碎而费力的工作,考虑到SQL语法的总量已经基本不再变化,相信假以时日,各方的差距会逐渐缩小。

除了SQL基本功,特别值得一提的是Fluent MyBatis的独门绝技:支持动态换表名(FreeQuery/FreeUpdate特性)。在云效项目的开发过程中,由于需要在各种嵌套查询之上再根据视图条件动态选择聚合计算的维度表,多亏了Fluent MyBatis的动态表名功能,才得以在最大程度保留语法构造便利性的情况下,让代码复用成为可能。

相比密密麻麻的XML文件,Java代码在易读性和可维护性方面有着明显的优势。在官方和社区的共同推动下,一个全新的、代码化的MyBatis生态正在冉冉升起。蓦然回首,曾经骄傲的"Plus扩展"们全都不香了。

二 优雅的数据流

初识Fluent MyBatis,最明显能感受到的特点是它及其便利的IDE语法提示。

基于数据表自动生成的Entity、Mapper、Query、Update等对象,让所有的数据库字段和SQL操作都变成了方法,串成平整的流式语句。即使是层层嵌套的查询,也能表现得错落有致:

new StudentQuery()
    .where.isDeleted().isFalse()
    .and.grade().eq(4)
    .and.homeCountyId().in(CountyDivisionQuery.class, q -> q
        .selectId()
        .where.isDeleted().isFalse()
        .and.province().eq("浙江省")
        .and.city().eq("杭州市")
        .end()
    ).end();

很容易就能看出,上述语句对应的SQL为:

SELECT * FROM student
WHERE is_deleted = false
AND grade = 4
AND home_county_id IN (
    SELECT id FROM county_division 
    WHERE is_deleted = false
    AND province = '浙江省'
    AND city = '杭州市'
)

不仅如此,Fluent MyBatis实现的JOIN语法经过几次调整后,现在的版本也已经十分美观:

JoinBuilder.from(
    new StudentQuery("t1", parameter)
        .selectAll()
        .where.age().eq(34)
        .end()
).join(
    new HomeAddressQuery("t2", parameter)
        .where.address().like("address")
        .end()
).on(
    l -> l.where.homeAddressId(),
    r -> r.where.id()
).endJoin().build();

其中利用Lambada语句表达JOIN条件的设计即充分符合了Java开发者的习惯,又很好的匹配了IDE语法提示的需要,细思极妙。

Fluent MyBatis中的流可以设置条件过滤,例如“仅更新值为非空的字段”:

new StudentUpdate()
    .update.name().is(student.getName(), If::notBlank)
    .set.phone().is(student.getPhone(), If::notBlank)
    .set.email().is(student.getEmail(), If::notBlank)
    .set.gender().is(student.getGender(), If::notNull)
    .end()
    .where.id().eq(student.getId()).end();

上面这段代码等效于MyBatis中的如下XML内容:

image.png

显然Java的流式代码可读性远高于XML文件的尖括号套尖括号的层叠结构。

流是可续接的,对于更复杂的分支条件,Fluent MyBatis中能利用譬如下述语句,充分发挥出Java代码的灵活性:

StudentQuery studentQuery = Refs.Query.student.aliasQuery()
    .select.age().end()
    .where.age().isNull().end()
    .groupBy.age().apply("id").end();
if (config.shouldFilterAge()) {
    studentQuery.having.max.age().gt(1L).end();
} else if (config.shouldOrder()) {
    studentQuery.orderBy.id().desc().end();
}

这种基于外部变量状态的判断,已然超出了MyBatis的XML文件的能力范围。

三 三分钟源码浅析

Fluent MyBatis的代码由Fluent Generator和Fluent MyBatis两个子项目组成。这对组合与MyBatis Generator搭档MyBatis Dynamic SQL有异曲同工之妙:Fluent Generator通过读取数据库里的表,自动生成Fluent MyBatis所需的Entity和Dao对象;Fluent MyBatis提供编写SQL语句的函数式DSL。

Fluent Generator子项目的代码显得朴实而平铺直述,程序入口在包结构树最外层的FileGenerator类型里,由开发者直接调用该类的build()方法,使用链式构造器方式传入需读取的表名和存放生成文件的目录等配置。Fluent Generator根据这些信息从数据库里读取出表结构,然后为每张表生成Entity和Dao类型的Java文件,放置到约定位置,整个逻辑一气呵成。值得一提的是,Fluent Generator的配置方法是完全代码化的,相比MyBatis Generator虽支持纯代码化配置,却在官方示例继续沿用XML文件配置输入的作风更胜一筹。

Fluent Generator生成的Dao类型默认是空的类,它只是一种推荐的数据查询层结构,通过继承各自的BaseDao类型,获得便捷操作Mapper的能力。

Fluent MyBatis子项目的代码要稍显丰盈一些,分为三个模块:

  • fluent-mybatis 包含各种公共基础类
  • fluent-mybatis-test 测试用例
  • fluent-mybatis-processor 编译期代码生成器

fluent-mybatis模块定义了与代码生成相关的注解、数据模型和其他辅助类型,它们大多都是幕后英雄:开发者通常不会直接用到这个包中的类。

fluent-mybatis-test模块包含丰富的测试用例,在一定程度上弥补了Fluent MyBatis当前阶段尚不完备的文档。平时遇到的许多Fluent MyBatis使用问题,若在文档上无法找到,那么翻一翻代码库的测试用例,一定会有意外的收获。

fluent-mybatis-processor模块的原理与Lombook工具库类似,但它并不修改原有的类型,而是扫描Entity类型上的注解,然后动态产生新的辅助类。Fluent Generator产出的Entity类就像是潘多拉盒子,蕴含着Fluent MyBatis魔法的秘密。FluentMybatisProcessor类是整场表演的魔术师,它将每个形如XyzEntity的实体类变幻出一系列辅助类,其中比较关键的包括:

  • XyzBaseDao:继承BaseDao类型,实现IBaseDao接口,包含获得Entity相关Mapper、Query、Update类型的方法,是Fluent Generator为用户生成的空白Dao类的父类。
  • XyzMapper:实现IEntityMapper,IRichMapper、IWrapperMapper接口,用于构造Query和Update对象,以及执行IQuery或IUpdate类型的SQL指令。
  • XyzQuery:继承BaseWrapper、BaseQuery类型,实现IWrapper、IQuery接口,用于组装查询语句的基本容器。
  • XyzUpdate:继承BaseWrapper、BaseUpdate类型,实现IWrapper、IBaseUpdate接口,用于组装更新语句的基本容器。
  • XyzSqlProvider:继承BaseSqlProvider类型,用于最终组装SQL语句。
  • 还有XyzMapping、XyzDefaults、XyzFormSetter、XyzEntityHelper:、XyzWrapperHelper等。由fluent-mybatis-processor模块生成的许多类型都会在编写业务代码的时候用到。

一个典型的Fluent MyBatis工作流程是先通过生成的Query或Update类型组装出执行对象,然后交给Mapper对象下发执行。譬如:

// 构造并执行查询语句
List<StudentEntity> users = mapper.listEntity(
    new StudentQuery()        .select.name().score().end()
        .where.userName().like("user").end()
        .orderBy.id().asc().end()
        .limit(20, 10)
);


// 构造并执行更新语句
int effectedRecordCount = mapper.updateBy(
    new StudentUpdate()
        .set.userName().is("u2")
        .set.isDeleted().is(true)
        .set.homeAddressId().isNull().end()
        .where.isDeleted().eq(false).end()
);

Query和Update类型不仅实现IQuery/IUpdate接口,还实现了IWrapper接口,前者用于组装对象,后者用于读取对象内容,这是一处很有心的设计。Mapper类型中的许多方法都能接收IQuery或IUpdate接口类型的对象,再通过方法上的@InsertProvider、@SelectProvider、@UpdateProvider或@DeleteProvider注解把实际请求转给生成的Provider类型。Provider们从约定的Map参数中取出传入的IWrapper执行对象,使用MapperSql工具类组装SQL语句,最后交给MyBatis执行。

在Mapper里也有一些直接接受Map对象的方法,可以省去用IQuery/IUpdate描述SQL的过程,进行简单的插入和查询。传入的原始Map对象同样会在Provider里被读取出来,用MapperSql组装SQL语句,再交给MyBatis执行。

Fluent MyBatis的这种基于Provider机制的实现方式不仅能为用户提供流畅的SQL构造体验,也能充分复用MyBatis原生的诸多优点,譬如丰富的DB连接器、健全的防SQL注入机制等等,从而确保核心逻辑的稳定可靠。

四 再见XML君

追求卓越是技术人的天性,我来自阿里云·云效产品团队,我们在用Fluent MyBatis。

如果您也早已厌倦MyBatis里毫无生气的XML文件,那么不妨就和它们做个告别吧。

Let's Fluent,加入飞速流动的队伍,一起来感受未来的风潮迎面吹来。


2021阿里云开发者大会资料

5月29日,2021阿里云开发者大会圆满结束。本次大会的主题是“云让应用创新更简单”,大会探讨了100+技术议题,涵盖开发与运维、云原生、大数据、人工智能、数据库、低代码等领域,阿里妹特意准备了大会议题PDF等资料,以便同学们回顾和学习大会核心技术内容。

扫码加阿里妹好友,回复“2021大会”获取吧~(若扫码无效,可直接添加alimei4、alimei5、alimei6、alimei7)

image.png

相关文章
Fluent Mybatis, 原生Mybatis, Mybatis Plus三者功能对比
使用fluent mybatis可以不用写具体的xml文件,通过java api可以构造出比较复杂的业务sql语句,做到代码逻辑和sql逻辑的合一。不再需要在Dao中组装查询或更新操作,在xml或mapper中再组装参数。那对比原生Mybatis, Mybatis Plus或者其他框架,FluentMybatis提供了哪些便利呢?
Fluent Mybatis, 原生Mybatis, Mybatis Plus三者功能对比
|
SQL XML IDE
Fluent mybatis
众多框架都是从无到有,从有到简的一个过程,核心理念都是为简化开发为生。
452 0
Fluent mybatis
|
SQL XML IDE
Fluent Mybatis 牛逼!做到代码逻辑和sql逻辑的合一
Fluent Mybatis 牛逼!做到代码逻辑和sql逻辑的合一
306 0
Fluent Mybatis 牛逼!做到代码逻辑和sql逻辑的合一
|
SQL XML JavaScript
Fluent Mybatis、原生Mybatis,、Mybatis Plus 大对比,哪个更好用?
Fluent Mybatis、原生Mybatis,、Mybatis Plus 大对比,哪个更好用?
|
XML SQL IDE
干掉 XML Mapper,新出的 Fluent Mybatis 真香!
干掉 XML Mapper,新出的 Fluent Mybatis 真香!
528 0
干掉 XML Mapper,新出的 Fluent Mybatis 真香!
|
1月前
|
Java 数据库连接 Maven
mybatis使用一:springboot整合mybatis、mybatis generator,使用逆向工程生成java代码。
这篇文章介绍了如何在Spring Boot项目中整合MyBatis和MyBatis Generator,使用逆向工程来自动生成Java代码,包括实体类、Mapper文件和Example文件,以提高开发效率。
115 2
mybatis使用一:springboot整合mybatis、mybatis generator,使用逆向工程生成java代码。
|
1月前
|
SQL JSON Java
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
这篇文章介绍了如何在Spring Boot项目中整合MyBatis和PageHelper进行分页操作,并且集成Swagger2来生成API文档,同时定义了统一的数据返回格式和请求模块。
56 1
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
|
1月前
|
前端开发 Java Apache
Springboot整合shiro,带你学会shiro,入门级别教程,由浅入深,完整代码案例,各位项目想加这个模块的人也可以看这个,又或者不会mybatis-plus的也可以看这个
本文详细讲解了如何整合Apache Shiro与Spring Boot项目,包括数据库准备、项目配置、实体类、Mapper、Service、Controller的创建和配置,以及Shiro的配置和使用。
333 1
Springboot整合shiro,带你学会shiro,入门级别教程,由浅入深,完整代码案例,各位项目想加这个模块的人也可以看这个,又或者不会mybatis-plus的也可以看这个
|
1月前
|
SQL Java 数据库连接
mybatis使用二:springboot 整合 mybatis,创建开发环境
这篇文章介绍了如何在SpringBoot项目中整合Mybatis和MybatisGenerator,包括添加依赖、配置数据源、修改启动主类、编写Java代码,以及使用Postman进行接口测试。
17 0
mybatis使用二:springboot 整合 mybatis,创建开发环境
|
1月前
|
Java 数据库连接 API
springBoot:后端解决跨域&Mybatis-Plus&SwaggerUI&代码生成器 (四)
本文介绍了后端解决跨域问题的方法及Mybatis-Plus的配置与使用。首先通过创建`CorsConfig`类并设置相关参数来实现跨域请求处理。接着,详细描述了如何引入Mybatis-Plus插件,包括配置`MybatisPlusConfig`类、定义Mapper接口以及Service层。此外,还展示了如何配置分页查询功能,并引入SwaggerUI进行API文档生成。最后,提供了代码生成器的配置示例,帮助快速生成项目所需的基础代码。