有了MyBatis,为什么还要搞一个Plus

📅 2026/7/2 3:34:27
有了MyBatis,为什么还要搞一个Plus
最近突发奇想想看看当年为啥有了mybaits后才会有人去折腾搞一个mybatis plus。肯定是有需求有痛点的才会有人去做。总是得有动机的。但是我最好奇的是最大最大的那个痛点是什么? 作者又是如何在架构设计层面解决了这个痛点思路是什么? 最核心的代码又是什么?作者肯定是要优先在架构设计这里思考如何入手的。我们今天不妨在一步一步的看一下。我先贴一段代码。// UserMapper.javaintinsert(Useruser);intupdateById(Useruser);intdeleteById(Serializableid);UserselectById(Serializableid);ListUserselectList();insertidinsertINSERT INTO user (name, age, create_time) VALUES (#{name}, #{age}, #{createTime})/insertupdateidupdateByIdUPDATE user SET name#{name}, age#{age} WHERE id#{id}/updatedeleteiddeleteByIdDELETE FROM user WHERE id#{id}/deleteselectidselectByIdresultTypeUserSELECT * FROM user WHERE id#{id}/selectselectidselectListresultTypeUserSELECT * FROM user/select每加一张表接口和XML往往要写成上面的样子。换Order、Product多半是复制粘贴只改表名和列名。程序员嘛都是希望能复用的就复用能少写的就少写不要让类似的代码满天飞失去控制。那表面上看有多痛?上面贴的那段代码本身不复杂麻烦就麻烦在量和频率。粗算一下在业务系统里通常都会有如下的固定成本维度说明规模N张表×约8个方法×约15行XML往往是数百行结构相同、仅表名列名不同的代码频率每个新项目、每次加表都要来一遍维护实体改字段XML、接口、实体三处同步列名写错编译期不报错痛点的本质就单表CRUD在MyBatis里被固定成一套重复动作这是最痛的地方。MyBatis帮你省掉的是后半段SQL怎么执行、结果怎么映射到实体这条链路它封装好了。运行模式是下面这样子Mapper接口方法 → MappedStatement → SqlSession → JDBCMappedStatement里挂着最终要执行的SQL。启动时MyBatis会把XML或注解解析成一个个MappedStatement放进Configuration缓存。到这里为止MyBatis的其中一个大贡献就很清楚了执行和映射不用你手写JDBC。缺的一环也在最前面。MyBatis不会替你造这些MappedStatement你要么在XML里一段段写insert、update要么在接口方法上挂Select、Insert。每一条都得自己声明id、写SQL、配好怎么映射成实体。还是上面User这张表光insert、updateById、deleteById、selectById、selectList这五样就要在XML或注解里各写一遍。业务里单表增删改查往往占大头而这类SQL骨架几乎同构差别主要在表名和列名。MyBatis把执行和映射做好了但是「注册MappedStatement」这一步的重复量它没帮你减掉。那要解决这个问题可以从哪里入手呢?解法思路既然每张表的CRUDSQL骨架几乎一样有差别的也就是表名和列名。那么能不能把表长什么样用某种方式描述出来让框架照着描述把那几段SQL生成好替掉手写的XML和注解这是一个「用描述替代手写」的方向。不过MyBatis原本执行的那套方案已经运行很多年了不能去动它的。动了等于另造一套ORM。所以生成的SQL最后还是用回MyBatis原来的路。这个约束决定了Plus只能在MyBatis的启动环节找切入点。顺着这个方向就有2个问题值得看一看表结构用什么描述、放哪生成出来的SQL要复用MyBaits的因此得了解MyBatis的执行原理。元数据从哪来先看第一个问题表结构用什么描述。Plus的做法是把描述信息直接写在实体类上。它定义了TableName、TableId、TableField几个注解用来标表名、主键、普通列。拿User举例TableNamepublicclassUser{TableIdprivateLongid;privateStringname;privateIntegerage;}不用在XML里写任何东西也不用在接口上写注解。实体上标好注解框架就知道这张表长什么样了。启动时TableInfoHelper会反射扫这些注解拼出一个TableInfo对象。表名、列名、主键是哪个、哪些字段参与insert全在这个对象里。可以把它理解成一张表的说明书后面生成SQL的时候需要它。注意这里和Select那类注解的区别。Select是在接口方法上直接写SQL描述的是「怎么查」。后面这组注解描述的是「表长什么样」SQL是框架根据描述生成的。方向不一样。元数据有了接下来就是第二个问题生成出来的SQL怎么塞进MyBatis?生成的SQL怎么进MyBatis前面提过MyBatis手写XML时启动期做了一件事把XML里的insert、update这些标签解析成MappedStatement放进Configuration缓存。运行期直接查缓存拿SQL来执行。Plus要做的就是在启动期往Configuration里多塞几个MappedStatement。塞进去的和XML解析出来的是一类东西。运行期完全不用改。那它是在什么时机塞进去的?Plus替换了MyBatis的MapperAnnotationBuilder换成了自己的MybatisMapperAnnotationBuilder。在parse()方法里执行顺序是这样的1. 加载同名的XML文件如果有的话 2. 遍历接口方法解析Select、Insert等注解 3. 如果接口继承了BaseMapper调用parserInjector()注入CRUD关键在第3步排在最后。XML和注解先parse手写的SQL先占坑。轮到注入CRUD的时候如果同一个方法名已经存在MappedStatement了就跳过。你在XML里自己写了insertPlus不会覆盖它。只做补充不抢你的活。这个顺序是有意的。实际项目里大部分表用默认CRUD就够了但总有些查询需要手写SQL。Plus的策略是你手写的优先级最高自动生成的只补你没写的部分。和前面讲的手写XML那条路比差别只在启动期手写XML时SQL是你自己写的用Plus时SQL是框架启动时生成的。运行期走的是同一条路没有任何差别。那注入这一步具体是怎么实现的?Plus怎么把SQL注进去看一下Plus和MyBatis原生在架构上的差异。层MyBatis原生MyBatis-Plus增加运行时执行SqlSession、Executor、JDBC不改Statement注册XML/Select解析SqlInjector启动注入元数据ResultMap、TypeHandlerTableInfo用户契约Mapper接口BaseMapper默认CRUD签名运行期那一层完全没动增强全在启动期。多出来的是SqlInjector这条线负责往Configuration里注入CRUD的MappedStatement。注入的时候框架得先搞清楚几件事这个Mapper对应哪张表表有哪些列主键是什么这些信息从Mapper接口的泛型参数里就能拿到。比如UserMapper extends BaseMapperUser泛型里的User就是实体类框架反射取出来交给TableInfoHelper去建TableInfo。建好TableInfo之后接下来就是给每个CRUD方法生成对应的SQL。insert、updateById、deleteById、selectList每个方法背后都有一个专门的类来负责。它们各自知道自己的SQL模板长什么样知道怎么从TableInfo里取列名和参数拼出来注册进去。拿Insert举例。它从TableInfo拿到列名列表和值列表套进INSERT的模板包成SqlSource注册成MappedStatement。关键代码就这几行// 拼列名和值片段StringcolumnScriptSqlScriptUtils.convertTrim(tableInfo.getAllInsertSqlColumnMaybeIf(null,ignoreAutoIncrementColumn),...);// 套进INSERT模板生成完整SQLStringsqlSqlMethod.INSERT_ONE.format(tableInfo.getTableName(),columnScript,valuesScript);// 包成SqlSource注册成MappedStatementSqlSourcesqlSourcesuper.createSqlSource(configuration,sql,modelClass);returnthis.addInsertMappedStatement(mapperClass,modelClass,methodName,sqlSource,keyGenerator,keyProperty,keyColumn);Update、Delete、SelectList都是同一个套路各自的子类拿各自的模板填空。用一张完整的时序图把启动过程走一遍小结回头看Plus做这件事最关键的设计决策是没有改MyBatis的运行期。所有增强都发生在启动期应用跑起来之后和手写XML走的是同一条路。这个选择挺务实的。MyBatis的运行期经过了多年验证动它风险大也没必要。Plus在启动期把SQL注册进去后面就完全交给MyBatis了对用户来说几乎零侵入。「用描述替代手写」这个思路挺常见的JPA的实体映射、Django ORM的Model都在做类似的事。Plus的价值在于它是在MyBatis的体系里找到的切入点。Plus没有另起炉灶而是在MyBatis的基础上把最重复的那部分工作自动化了。参考的内容MyBatis-Plus官方文档