大型网站制作方案,网络推广策划培训班,高端网站设计服务商,如何保护网站名简介#xff1a; 只需瞅一眼Google Trends上全球Java界最热门的两款SQL映射框架近一年的对比数字#xff0c;就不难了解其实力分布#xff1a;在此领域#xff0c;MyBatis早已占领东亚地区开发者市场#xff0c;并以绝对优势稳居中国最抢手Java数据库访问框架之首。 作者 …简介 只需瞅一眼Google Trends上全球Java界最热门的两款SQL映射框架近一年的对比数字就不难了解其实力分布在此领域MyBatis早已占领东亚地区开发者市场并以绝对优势稳居中国最抢手Java数据库访问框架之首。 作者 | 金戟 来源 | 阿里技术公众号
只需瞅一眼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 QueryWrapperStudentScore().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 falseAND 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内容 显然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实现IEntityMapperIRichMapper、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对象下发执行。譬如
// 构造并执行查询语句
ListStudentEntity 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文件那么不妨就和它们做个告别吧。
Lets Fluent加入飞速流动的队伍一起来感受未来的风潮迎面吹来。
原文链接 本文为阿里云原创内容未经允许不得转载。