团队去年全面切MyBatis-plus,想着CRUD不用写了,效率翻倍。结果半年后,复杂业务场景里还是手写SQL,而且比原来更绕。整理了5个让MyBatis-plus"失效"的场景,不是框架不好,是用错了地方。
场景一:多层嵌套子查询
MyBatis-plus的QueryWrapper链式调用,两层嵌套还能看,三层以上就是灾难
// 两层勉强能读 queryWrapper.eq("status",1).in("id", new QueryWrapper<Order>().select("user_id").eq("amount",100));// 三层?我放弃了,直接写XML实际业务:查"最近30天消费超过500元且购买过指定品类且退货率低于10%的用户"。三层子查询+聚合+条件,QueryWrapper写出来像天书,维护的人骂娘。
手写SQL:
<selectid="findQualifiedUsers"resultType="User">SELECT u.* FROM user u WHERE u.id IN(SELECT o.user_id FROM`order`o WHERE o.create_time>DATE_SUB(NOW(), INTERVAL30DAY)AND o.amount>500AND o.id IN(SELECT oi.order_id FROM order_item oi WHERE oi.category_id=#{categoryId})AND o.user_id NOT IN(SELECT r.user_id FROM refund r WHERE r.rate>0.1))</select>看着长,但结构清晰,SQL优化器也能看懂。MyBatis-plus生成的SQL,三层嵌套后逻辑混乱,执行计划全表扫描。
场景二:动态表名/动态列
分库分表场景,表名按月份拆分: order_202601 、 order_202602 。
MyBatis-plus的 @TableName 注解不支持动态表名,除非自己写拦截器替换SQL,但那样还不如直接写XML。
<selectid="statReport"resultType="Map">SELECT province, city, channel, DATE(create_time)as dt, COUNT(*)as order_cnt, SUM(amount)as total_amount, AVG(amount)as avg_amount, SUM(CASE WHENstatus=9THEN1ELSE0END)/COUNT(*)as refund_rate FROM`order`<where><iftest="province != null">AND province=#{province}</if><iftest="startDate != null">AND create_time>=#{startDate}</if></where>GROUP BY province, city, channel, DATE(create_time)WITH ROLLUP</select>WITH ROLLUP 做维度汇总,MyBatis-plus不支持这种语法,只能原生SQL。
场景四:批量插入优化
MyBatis-plus的 saveBatch 默认是一条条INSERT,批量1000条数据,数据库往返1000次。
// 默认实现,性能极差 userService.saveBatch(userList);//1000次网络往返想优化?自己写 INSERT INTO … VALUES (…), (…), (…) ,或者配置 rewriteBatchedStatements=true ,但MyBatis-plus不帮你做这个。
手写SQL:
<insertid="batchInsert">INSERT INTO user(name, email, create_time)VALUES<foreachcollection="list"item="item"separator=",">(#{item.name}, #{item.email}, NOW())</foreach></insert>一次网络往返,性能提升100倍。
场景五:数据库特定语法
MySQL的 ON DUPLICATE KEY UPDATE 、PostgreSQL的 ON CONFLICT 、SQL Server的 MERGE ,MyBatis-plus的通用CRUD不支持这些方言。
<insertid="upsert">INSERT INTO user(id, name, email)VALUES(#{id}, #{name}, #{email})ON DUPLICATE KEY UPDATE name=VALUES(name), email=VALUES(email), update_time=NOW()</insert>我的用法
不是不用MyBatis-plus,是分层用:
简单CRUD、单表查询、快速原型:MyBatis-plus,省代码
复杂查询、子查询、聚合、批量、方言特性:手写XML,可控可优化
一个反直觉的结论
MyBatis-plus最大的价值,不是让你不写SQL,是让你明确知道哪些SQL不用写。简单场景省时间,复杂场景不折腾,边界清晰。
如果所有SQL都手写,和MyBatis有什么区别?如果所有SQL都用MyBatis-plus,复杂业务就是灾难。
最后说个数据
我们团队代码统计:MyBatis-plus占60%,手写XML占40%。但Bug率,手写XML的部分反而更低,因为复杂逻辑显式表达,review时容易发现问题。MyBatis-plus生成的SQL,有时候执行计划全表扫描,直到线上慢查询才发现。
工具是帮你省时间的,不是帮你思考的。复杂场景,手写SQL是负责任的选择。