只要看看近一年全球Java界最流行的两个SQL映射框架在谷歌趋势上的对比图,就不难理解它们的强弱分布:这里字段,MyBatis已经占领了东亚地区的开发者市场,并以绝对优势稳居中国最抢手的Java数据库访问框架第一名。MyBatis称霸榜单的底气来自于其广阔的生态和国内众多厂商的支持。在琳琅满目的MyBatis扩展中,还埋藏着不少“宝藏项目”,来自阿里技术团队的FluentMyBatis就是其中独树一帜的新星。1.加上人不受欢迎。从iBatis到MyBatis,再到以国内团队MyBatisPlus为代表的众多周边工具,“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的主要贡献者之一JeffButler正式创建了MyBatisDynamicSQL项目,MyBatis终于开始全面拥抱XML-free编码的SQL构建。在MyBatis与MyBatisDynamicSQL的6年多时间间隔期间,开源社区催生了很多基于MyBatis的非XML免代码解决方案,其中最受欢迎的是TkMybatis和MyBatis等内置程序加。Mapper和自动生成CRUD的扩展库一经推出就获得了很多好评。包括MyBatisPlus中不完善的“ConditionalBuilder”功能,也是因为当时没有类似的解决方案而大受欢迎。同时,在MyBatis社区之外,一直默默发展的JOOQ是一个纯Java的动态SQL执行库,其历史几乎与MyBatis一样悠久。它的用户群很小,但口碑很好。如今,如果你在任何一个搜索引擎上输入“MyBatisvsJOOQ”,你仍然可以得到几乎片面的选择JOOQ的结果。大家给出的理由也很一致:简洁,灵活,不需要XML,很“Java”。在MyBatis阵营中,如果你拿出MyBatisPlus的“ConditionBuilder”来正面迎战,仅仅三回合你就会被踢出擂台。可惜JOOQ的家世不如MyBatis,早早走上了商业数据库支持卖授权费的道路,才让MyBatis免于迎来自己的舆论中年危机。FluentMyBatis诞生于2019年年底,虽然与MyBatisDynamicSQL相比属于初级,但仍处于成长期,已经初显绿比蓝、胜于蓝的滋味。在实现上,MyBatisPlus重写和替换了MyBatis内部类型的一些方法。整体机制比较重,但也可以在内部逻辑中隐藏一些用户不需要关注的功能细节。相反,MyBatisDynamicSQL的实现机制非常轻量级。不仅完全基于MyBatis独创的Provider系列注解开发,而且没有任何隐藏逻辑。针对用户的每张表,自动生成对应的Entity、DynamicSqlSupport、Mapper三个类,都放在用户的源码目录下,所以暴露了更多细节,代码侵入性稍强。FluentMyBatis取两者之长,整体机制更接近MyBatisDynamicSQL。也是基于原生的Provider注解,为用户的每张表生成Entity类和默认空白Dao类。不同的是,它还会通过JVM的编译时代码增强功能,自动生成许多开发者无法更改的标准辅助类。这些代码不需要放在用户的源代码目录下,编码时直接使用,既提供了丰富的功能,又保证了用户代码的整洁。在使用上,FluentMyBatis也借鉴了前辈的最佳实践。没有花哨的注解和配置,直接复用MyBatis连接。所有功能开箱即用。同时,由于FluentMyBatis以方法调用的形式提供了所有的表字段、条件和操作,因此比其他同类项目有更好的IDE语法辅助。举个不太复杂的例子://使用FluentMyBatis构造查询语句mapper.listMaps(newStudentScoreQuery().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(newString[]{"English","数学","Chinese"}).and.score().ge(60).and.isDeleted().isFalse().end().groupBy.schoolTerm().subject().end().having.计数.score.gt(1).end().orderBy.schoolTerm().asc().subject().asc().end());MyBatisDynamicSQL的语法也比较优美,但是字段名和min/max/avg等方法需要静态引用,与FluentMyBatis相比略逊一筹。//使用MyBatisDynamicSQL构造查询语句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方法当前没有supported.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("English","Mathematics","Chinese"),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();MyBatisPlus的条件构造器只封装了基本的SQL操作,对于字段、条件、别名等必须用字符串拼接,容易出现拼写错误导致的SQL异常。//使用MyBatisPlus构造查询语句mapper.selectMaps(newQueryWrapper().select("school_term","subject","count(score)ascount","min(score)asmin_score","max(score)asmax_score","avg(score)asavg_score").ge("school_term",2000).in("subject","English","Mathematics","Chinese").ge("score",60).eq("is_deleted",false).groupBy("school_term","subject").having("count(score)>1").orderByAsc("school_term","subject"));Java动态SQL内置功能完备程度,目前排名为MyBatisPlusq.selectId().where.isDeleted().isFalse().and.province().eq("浙江省").and.city().eq("杭州市").end()).end();不难看出,上述语句对应的SQL为:SELECT*FROMstudentWHEREis_deleted=falseANDgrade=4ANDhome_county_idIN(SELECTidFROMcounty_divisionWHEREis_deleted=falseANDprovince='浙江省'ANDcity='杭州市')不仅如此,FluentMyBatis实现的JOIN语法还有多次调整,现在的版本也很漂亮:JoinBuilder.from(newStudentQuery("t1",parameter).selectAll().where.age().eq(34).end()).join(newHomeAddressQuery("t2",parameter).where.address().like("address").end()).on(l->l.where.homeAddressId(),r->r.where.id()).endJoin().build();使用Lambada语句来表达JOIN条件的设计,既完全符合Java开发者的习惯,又能很好的匹配IDE语法提示的需要,非常周到。FluentMyBatis中的流程可以设置条件过滤,比如“只更新值不为空的字段”:newStudentUpdate().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文件中尖括号内部尖括号的级联结构可读性高很多。流动是连续的。对于更复杂的分支条件,FluentMyBatis可以使用如下语句,例如充分发挥Java代码的灵活性:StudentQuerystudentQuery=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();}elseif(config.shouldOrder()){studentQuery.orderBy.id().desc().end();}这种基于外部变量状态的判断,它超出了MyBatis的XML文件的能力。三三分钟源码分析FluentMyBatis的代码由两个子项目组成,FluentGenerator和FluentMyBatis。这种组合与MyBatisGenerator搭档MyBatisDynamicSQL效果相同:FluentGenerator通过读取数据库中的表,自动生成FluentMyBatis需要的Entity和Dao对象;FluentMyBatis提供了用于编写SQL语句的函数式DSL。FluentGenerator子项目的代码简单明了。程序入口在包结构树最外层的FileGenerator类型中。开发者直接调用该类的build()方法,使用链式构造函数传入读取的表名、生成文件存放目录等所需配置。FluentGenerator根据这些信息从数据库中读取表结构,然后为每个表生成Entity和Dao类型的Java文件,并放置在约定的位置,整个逻辑一气呵成。值得一提的是,FluentGenerator的配置方法是完全编码的。与支持纯编码配置的MyBatisGenerator相比,优于官方示例中继续使用XML文件配置输入的风格。FluentGenerator生成的Dao类型默认是一个空类。它只是一种推荐的数据查询层结构。通过继承各自的BaseDao类型,获得了方便操作Mapper的能力。FluentMyBatis子项目的代码稍微丰富一些,分为三个模块:与代码生成相关的注释、数据模型和其他辅助类型大多是幕后英雄:开发人员通常不会直接使用这个包中的类。fluent-mybatis-test模块包含了丰富的测试用例,一定程度上弥补了现阶段FluentMyBatis文档的不完善。很多你平时遇到的FluentMyBatis使用问题,如果在文档中找不到,那么翻翻代码库中的测试用例,一定会有意想不到的收获。fluent-mybatis-processor模块的原理和Lombook工具库类似,只是不修改原来的类型,而是扫描Entity类型上的注解,然后动态生成新的辅助类。FluentGenerator产生的Entity类就像潘多拉的盒子,蕴藏着FluentMyBatis魔法的秘密。FluentMybatisProcessor类是整个表演的魔术师。它将像XyzEntity这样的每个实体类转化为一系列的辅助类。关键的包括:XyzBaseDao:继承BaseDao类型,实现IBaseDao接口,包括获取Entity相关的Mapper和Query,Update类型的方法,是FluentGenerator为用户生成的空白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模块生成的类型在写业务代码的时候都会用到。典型的FluentMyBatis工作流是先通过生成的Query或Update类型组装执行对象,然后交给Mapper对象执行。例如://构造并执行查询语句Listusers=mapper.listEntity(newStudentQuery().select.name().score().end().where.userName().like("user").end().orderBy.id().asc().end().limit(20,10));//构造并执行update语句ineffectedRecordCount=mapper.updateBy(newStudentUpdate().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中读取,SQL语句用MapperSql组装,然后交给MyBatis执行。FluentMyBatis基于Provider机制的实现,不仅可以为用户提供流畅的SQL构建体验,还可以充分复用MyBatis原有的诸多优势,如丰富的DBconnectors、完善的SQL注入预防机制等,从而保证核心逻辑的稳定性和可靠性。