Mybatis 的缺陷

2019-03-08

Java

Mybatis 的优势是由于采用模板映射,可以更自由的控制SQL语句和映射,可以使用很多的临时解决方案。但也真因为

采用模板的自由化,导致数据库字段发生变更时会产生很大的问题。

Mybatis是阿里这种大公司的御用框架,于是理所当然的成了绝大多数公司的标准选择

(理由很简单"大公司都在用,我们用,没毛病"...),尤其是这些年,很多人放弃了数据库的范式,极端追求数据库表的"扁平化",

大量使用扁平的表结构,去掉关联关系,大量使用冗余,很多所谓的架构师还理所当然的说在服务化的场景下这是绝对正确的;

很多人说java需要分层,什么view,facade,service,dao...这还不够,

于是最终瞄准了DAO层~他们说将SQL拿出来独立出一个mapper层,说这样更容易管理,在代码里拼装SQL是上个世纪的做法,是一种很low的行为;

反正..个人无法苟同这种无脑的逻辑;

我曾经无数次劝说别人放弃Mybatis这个坑;实际上,Mybatis让我的代码变得很糟糕,还让我重复干了好多事情~

  1. Mybatis是什么?毫不客气的说,这货只是一个"SQL模板引擎",而不是一个完整的DAO解决方案;为什么我要使用DAO框架去处理一些东西?因为我懒啊,如果什么都需要我自己干我还需要它干嘛??有人说Mybatis generator~Mybatis generator能解决你80%的问题吗?如果Mybatis generator能解决你80%的问题,甚至你说你根本不需要自己写SQL,使用生成的就好,那么..nutz这种简单轻量的框架不是更好???相比直接使用JDBC API,Mybatis没有让事情变得更加单(你甚至可以自己写一个jdbc generator根据数据库去生成DAO代码,我相信这大多数人都能做到)你要写的依然需要自己写,甚至你需要来回维护多份东西(数据库DDL,数据库文档,SQL XML,你的Entity代码,你的dao(或者mapper)接口…);

想想当年,直接用hibernate写entity建模,生成DDL和常规通用接口代码,jekins+自己写的数据库文档生成工具,自动根据entity注释和数据库元数据生成markdown文档...不要太惬意哦;为什么我还要花那么多时间搞一个半自动的东西?(我的习惯是直接用java代码建模,然后直接生成数据库(物理模型),我甚至不需要手动添加索引,觉得entity哪个字段看着顺眼会用于查询加个注解就完了,然后用自己写得小工具生产通用dao层和数据库文档...很多事情只需要做一次就行了,用过hibernate建模的估计都知道我在说什么~)

很多人拿Mybatis和hibernate比,说Mybatis怎么怎么好..我只能说,呵呵了;这两者根本不是一个层面的东西好吧~

  1. mybaties使用XML这种"非人类的东西(众所周知XML这种东西设计目标就不是给人阅读的)"作为模板语言,代码可读性可维护性多糟糕你自己打开Mybatis generator生成的东西或者别人写的mapper文件看看你就知道了,你还说好维护?呵呵了,我直接在java里拼接SQL可读性都比他好;XML中对大于">""<"这种特殊字符串还需要做转义处理,使得可读性变得更加糟糕;
<if test='id != null and id gt 28'></if>

//这种方式和拼接字符串本质又有啥区别吗????不明白比在java中拼接字符串高明在哪里~
//对于Java中拼接,还能用Java的语法优势,我甚至不需要熟悉MYBaXXX的XML语法~
<select id="selectUseIf" parameterType="com.DynamicTestModel" resultMap="userMap">
select * from t_user where 1=1
<if test='id != null'>
and id=#{id}
</if>
</select>

你们觉得这种代码比拼接字符串高明在哪里?更容易阅读吗????

  1. 无病呻吟的封装;

一个DAO方法往往有多个参数,有些简单的接口,比如:

/**

  • getUser by username
  • @param username
  • 用户名,不能为空或者emptyStr
  • @param tenantId
  • 租户ID,不能为空或者emptyStr
  • @return 返回租户下username对应的的用户,如果不存在返回null
  • /

User getUser(String username,String tenantId);
我一直强调封装是必须有语义的,语义不随接口变化而变化,他在系统中存在单一的语义,同时最好能够复用~

比如上面这个 User,你一看名称就知道它代表一个用户,这也符合单一功能原则~这个User在其他接口;比如

List listUsers(int first,int max) ;
也能重用~所以我觉得User是一个不错的封装~而下面的GetUserTo 就不是一个好的选择~

这个接口语义很简单(也很明确,调用它的人一看就知道要干嘛):根据username和租户id(做过多租户saas软件大概明白这是干嘛的)获得一个User对象~

在mybaties中我们需要怎么办做呢?使用注解标记区分username和tenantId两个参数;

User getUser(@param(“username”) String username,
@param(“tenantId” String tenantId);//多了一个注解,看着也还能接受~
或者,使用一个专门的TO或者map进行封装;

User getUser(GetUserTo to);//个人觉得这不是好的封装他的语义是接口级别的;其他接口而已这个封装几乎没有任何用处
User getUser(Map to);//上面的封装还能忍,无非多几个不能复用的,没有语义的,一次性的to对象,
//调用者跳入GetUserTo依然还能通过属性上的注释看到接口参数约束,可这个Map真的我就无法忍受了

再或者放弃干脆放弃命名参数,使用下标~#{0}

多几行代码是小事情,可,这样真的优雅吗??因为这个坑,我需要放弃一个优雅的模型和接口,从上到下提供各种蛋疼的封装~

题外话:实际上,出现这个问题的原因也不能怪mybaties~我们知道我们通过反射可以获得方法的名称,参数类型和返回类型等信息(编译器和连接器通过参数个数和类型区分重载方法,不需要参数名称,所以参数名称在编译后被丢弃了),甚至泛型信息等元数据,但我们无法获得一个接口的"参数名称"(至少早期的java版本是这样的),class文件中的参数名称是arg0 arg1;运行时候这些参数名称虚拟机也不会使用和保留;

那么?除了注解,有什么办法在运行时获得接口参数吗?

java6的时代(那时候自己在写一个框架,需要获得接口参数名称)我费劲心思找到了一个 paul-hammant/paranamer ;这个东西~

实际上,(基本思想:你无法从class中获得这些元数据,那么你只能从源代码出发,在编译的时候获取这部分数据了~)你可以解析你的源代码,然后从源代码中抽离出方法的参数名称,生成一个参数名称映射文件,运行的时候读取这个额外生产的映射文件来获取参数名;

这种方式的好处是在于兼容性;而,问题是在于,每次改变接口你都需要重新生成一遍这个映射文件;

对于java8可以添加 -parameters 编译参数,让编译器保留参数名称信息;但这依然会带来很多问题,比如字节码变大,兼容性等;

以上两种方法都不是很好,所以mybaties只能选择用目前提供的三种比较恶心的方式去处理这个问题了~

  1. 很难优雅的管理区分"手写SQL XML"和"自动生成的SQL XML";

很多时候我们使用Mybatis generator生成通用的mapper,如果将自己的SQL也写在里面,重新生成的时候会覆盖掉我们的自定义的东西,或者需要重新合并,非常麻烦;所以对于特殊的需求我们去要用另一个XML文件,分开管理他们~这已经是目前能想到的最优雅的方式;可..真的优雅吗?反正我觉得挺别扭的~

知乎用户:数据库和ORM如何优雅的添加字段?

  1. 代码生成是"编译时(实际上是编译前)"而"非运行时";

很多小公司往往:a,需求不明确;b,开发时间紧,没过多的时间做完善的设计;

于是,代码写着写着需要添加一个字段两个字段是常有的事儿;于是你需要:a,修改数据库增加字段,生成修改SQL;2,运行generator重新生成一遍;3,手动修改自定义部分,往往有多处地方修改(自定义的TO.XML多出地方包括头上的映射信息,手写SQL里面的各种地方),一不小心就改错了或者漏了(我相信任何用过Mybatis的人都遇到过这个问题)~;4,你可能需要维护一份数据库文档,然后你得打开你的word文档,然后添加一个字段~

....

老这么折腾,你的时间是多没价值啊?

我们知道hibernate,包括国产的nutz这种框架在运行时生成SQL的~将SQL的生成推迟到运行的时候;

这种"延迟SQL生成"的方式或许有有那么一点点的性能上代价(生成的SQL是有缓存的~而且你真的无法忍受吗?)

可带来的好处是很直观的~添加一个字段?我在entity中加个字段就OK了?

我需要干别的吗?或许也需要写个文档啥的,出数据库文档你自己不能写个工具吗?或者PD??

  1. 调试问题/代码重构

拼接字符串这种丑陋的方式至少能调试的???可你告诉我?放在XML中的SQL怎么调试??????

用java写的好处是java能在语法级别给出错误提示,当然还有IDE强大的重构功能;XML?搜一下关键字然后一行一行改吧~

  1. 手写的SQL未必比hibernate生成的SQL效率高~

可能因为项目时间的关系,有的人写SQL就不怎么讲究了,比如select * 这种不会出现在hibernate生成的SQL里~

  1. 缓存问题~

(这里说的是二级缓存,不是thread级别的缓存)

hibernate缓存?我只需要简单配置一下 ?就像这样?

<!-- Secondary Cache -->
<property name="hibernate.cache.use_second_level_cache">true</property>
<property name="hibernate.cache.use_query_cache">true</property>
<property name="hibernate.cache.provider_class">
org.hibernate.cache.EhCacheProvider
</property>

我就能简单开启二级缓缓存了~包括查询缓存(KEY是SQL),数据缓存(KEY是实体ID)

替换掉这个EhCacheProvider,你就可以用你喜欢的任何缓存,比如OSCache,Memcached,或者redis…了~(当然,用的不好还是会出问题的,尤其是那个查询缓存,慎重~)

mybaties缓存?你自己慢慢写吧~

事实上~mybaties这种不完备的非实体映射的DAO框架限制了你大概只能"显式"的调用缓存接口~

  1. 分表分库/审计/全文索引问题~

hibernate shards;hibernate Envers;hibernate search;...

hibernate有一系列的解决方案帮你做这些事情;

mybaties???你自己慢慢写吧~

  1. 糟糕的实现;

曾经简单看了一下mybaties的代码~因为时间长了,代码质量好不好已经不太记得了;

不过:我记得当时为了数据加密需要实现自定义的拦截器,看了一下mybaties拦截器的实现~

发现拦截方法只有update(还有其他几个),并没有insert和delete(他们都走update逻辑,代码实现也是调用同一个方法)~

有人提出这是个bug,Mybatis并没有承认这是个bug~(不知道现在什么情况)

  1. 关联查询

Mybatis 连接多张表?还能自动生成了吗?好吧,我觉得这是噩梦~至少比DBUtils或者手写SQL来说要麻烦的多得多~

于是你说,我们不需要关联表,面向服务的开发是不需要关联的,不需要任何外键约束的,关联表是不合理的…..balabla….

好吧,你赢了~或者你的业务足够简单~

  1. 满世界找文件~

文件多了,很多东西分开或许是好事,或许不是,因为这意味着,你需要这一个东西你得满世界找,本来改一个地方,你现在需要改多出地方;很多东西是有代价的,分层,业务拆分,服务拆分...需要找一个平衡点,而非毫无底线,越细越好~好的方式一定是简单的,我们是从实际需求出发,而非从某个完美设计或者构想出发,技术是为人类服务的,而不是折腾人的,很多东西是没必要的,比如XML中写SQL这种东西,你说分开容易管理,我只能呵呵了~