|
很久没用 Java 做后端开发了, 最近一个项目用 Spring-Boot + MyBatis + MyBatis-Plus (简称MP) 来做开发, 于是就碰到了很多坑, 我重点说几个问题, 望对后来者有用.
<hr/>一、文档垃圾
MyBatis-Plus给我的第一观感是文档垃圾, 官方文档似乎还比较推崇不知道的就去读源码, 这实在是国内一些“源码论”人士的糟粕思想. 因为文档缺乏相关的信息, 或者更新不及时, 或者设计不合理, 以至于用户不得不去通过读源码去完成工作, 这是好事吗?
以下是“条件构造器”的文档说明:
- 以下出现的第一个入参boolean condition表示该条件是否加入最后生成的sql中
- 以下代码块内的多个方法均为从上往下补全个别boolean类型的入参,默认为true
- 以下出现的泛型Param均为Wrapper的子类实例(均具有AbstractWrapper的所有方法)
- 以下方法在入参中出现的R为泛型,在普通wrapper中是String,在LambdaWrapper中是函数(例:Entity::getId,Entity为实体类,getId为字段id的getMethod)
- 以下方法入参中的R column均表示数据库字段,当R具体类型为String时则为数据库字段名(字段名是数据库关键字的自己用转义符包裹!)!而不是实体类数据字段名!!!,另当R具体类型为SFunction时项目runtime不支持eclipse自家的编译器!!!
- 以下举例均为使用普通wrapper,入参为Map和List的均以json形式表现!
- 使用中如果入参的Map或者List为空,则不会加入最后生成的sql中!!!
- 有任何疑问就点开源码看,看不懂函数的点击我学习新知识
你看这些描述, 完全是一脸懵逼. 下面就是各种函数的列表和参数说明, 就是一些自动生成的文档, 没什么用处.
我来打个比方, 比如第一个 boolean condition, 很多地方都有, 以上的说明里就提了一下.
我的第一反应是: “你说的这个谁懂啊!” 实际上它的作用是这样:
你经常会写这样的代码:
if (StringUtils.isNotBlank(name)) {
query.like(Entity::getName, name)
}
if (age != null && age >= 0) {
query.eq(Entity::getAge, age)
}就是如果没有传name参数, 其实是没有必要添加这个条件的. 满足一定条件才会把查询条件加上去. 写的多了, 就很麻烦, 而用MyBatis-Plus的构造器, 你就可以这么写:
query.like(StringUtils.isNotBlank(name), Entity::getName, name)
.eq(age!=null && age >= 0, Entity::getAge, age)这第一个参数就叫做condition. 你看, 不用反复的用if条件判断, 还可以把条件串联者写.
这么常见的功能, 且重要的设计, 没有什么篇幅, 一句话就过了, 之前没接触过的, “谁知道这些condition都是干啥的”.
同理, 以下每一条都可以展开说明一下设计思想, 为什么这么设计, 都是干啥用的. 这些一概没有. 这才是需要文档的啊, 而不是自动生成的API列表!
所以我给它的文档一个“垃圾”的评价, 是有理有据的.
二、强制的架构
MyBatis-Plus自称只是加强MyBatis, 不限制它的使用. 实际上你想用得爽, 你必须接受 MyBatis-Plus 的架构:
这就是很流行的一种思想, 估计是由阿里传出来的, 什么表不能有外键啊、设计简单不关联、所有业务逻辑不能放数据库啊, 等等. 这种事情很常见. 一般做这种优化的, 都是业务场景不复杂, 但是性能要求和数据量都非常大, 于是要做这种妥协.
但是对于大部分做业务开发的来说, 完全是不同的场景, 通常是数据结构复杂, 业务流程复杂, 而用户量和数据量却有限, 很容易出现数据不一致性. 这种情况下, 完全没有必要采用以上妥协.
很多人以此为借口, 不好好学习关系数据库的精髓, 而照搬照用. 科学大牛创造了关系理论, 工程大牛们创造了关系数据库, 具有丰富的功能, 帮助你确保数据一致性, 帮助你搞定数据建模问题, 结果到了你这, 啥都不用, 把关系数据库(MySQL)当作NoSQL(MongoDB)来用!
我跟你们说一下如果你违背了这个设计前提, 会碰到什么结果. 如果你的表有left join, 你的分页很可能是无法返回正确结果的! 之所以说很可能, 如果left join的表, 没有重复的外键, 那么是OK的, 例如:
user
id, name, ...
user_info
id, user_id, org, ...
因为user_info里, 每个user_id只有一行数据, 所以当你left join到 user 表中, 获取额外信息时, 不会造成问题. 但是如果你要有这样的表:
user_roles
id, user_id, role
1, 1, &#34;User&#34;
2, 1, &#34;Admin&#34;
我们看到userroles中, 一个用户有两个角色, 如果你想通过一个查询语句返回用户列表并带出user_roles, 那么需要使用 MyBatis 的 collection 元素来自己写XML, 那么分页插件就会失效. 你查出的数量是role的数量, 而不是user的数量.
如果你有多主键的表, MP是不支持的, 你最好自定义一个额外的自增主键, 然后使用哪个主键去做增删改, 然后使用Wrapper条件去做查找. 如果你有自定义的 TypeHandler, 需要在定义实体时(TableName注解), 添加 autoResultMap = true 这个选项. 否则你的 TypeHandler 只在保存和更新时有效, 查询的时候无效. 如果你想复用 MP 自动生成的ResultMap, 需要从源码里查找那个ResultMap是怎么命名ID的. 从ID规则来看, MP是没想让用户这么用的. 也从另一个侧面证明了, MP 是一个伪装成 SpringFramework 的 SpringBoot, 它对你已经有预设了, 但是却说自己“只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑”。确实, 如果你整体的数据架构都是基于NoSQL来做的(也就是没有设计), 你会有非常爽的感受. 如果你是一个真正的关系数据库用户, 你会痛不欲生.
如果我要给 MyBatis-Plus 换个更合适的名字, 可以叫做 MyBatis-NoSQL 或者 MyBatis-MongoDB. 如果你用过 MongoDB, 又碰巧用过 MP, 你会发现这种设计哲学上的相似性. 如果你在选型, 你要好好考虑一下.
三、设计细节还有待完善
一些小细节, 影响不大, 但是还是值得一提, 比如QueryWrapper, 直接使用字符串作为key, 还可以直接设置SQL语句作为条件, 在API层面混用不同的设计(属于leaky abstraction), 这样IDE无法对字段有效性做检查, 非常容易出错. 因此我推荐使用LambdaQuery, 至少保证类型正确.
API接口欠考虑, 如果你想用多个字段排序, 接口如下:
query.orderByAsc(Entity::getNumber, Entity::getCreateTime);这时IDE(java compiler)会给warning: Unchecked generics array creation for varargs parameter. 一个库的接口, 正确使用, 会出现warning, 这是不可接受的. 有追求的程序员, 会要求自己的代码没有warning, 更别说一个广泛使用的库了.
半吊子功能, 比如乐观锁插件, 只支持在Update时校验Version, 不支持删除时校验Version (当然逻辑删除本质上是update, 不知道是否可以透明使用这个校验). 既然声称有这个功能, 就弄得完整一点, 常见的场景不覆盖, 有点不太合适.
四、总结
当然MyBatis-Plus在其他方面有很多值得夸赞的地方, 我就不提了, 因为本篇主要是记录我遇到的坑. 可能有些苛刻, 但是我希望能帮助到一些之前没有接触过MP的朋友. 如果你的数据架构跟MP的设计思想一致, 我强烈建议你使用. 否则我建议使用MyBatis Generator来简化你的常见单表操作, 或者使用JPA (或者Spring Data JPA). |
|