Freewind @ Thoughtworks scala java javascript dart 工具 编程实践 月结 math python english [comments admin] [feed]

(2011-09-13) Mybatis体验

广告: 云梯:翻墙vpn (省10元) 土行孙:科研用户翻墙http proxy (有优惠)

话说上次用Hibernate,发觉性能问题处理起来很麻烦之后,我又拾起了Mybatis。Mybatis曾经叫作ibatis,它与Hibernate同时代,但以Hibernate相反的方式来实现orm(准确的说,叫mapper)。即仍然以SQL为重心,但自动将结果集装配成pojo(当然配置是少不了的)。

  Mybatis的特色是:1。手写所有的SQL 2。SQL写在XML文件里 3。pojo与数据表的映射关系,以及与结果集的映射关系,也写在XML中。4。当我们需要操作数据库时,在java代码中,调用XML文件中定义的某SQL的ID,同时传参数进去。


  与Hibernate相比,使用mybatis的人少得多,但也有不少死忠。它的显著优点是:你可以完全自由的写SQL,可以使用某些数据库特定的写法,甚至存储过程,来实现高性能的数据库操作。比如N+1问题,在mybatis中也存在,但是我们完全可以手写复杂的join语句来避免。因为mybatis中的所有SQL都是手写的,你在写的时候会自然而然地使用掌握的技巧,写出高性能的SQL,哪怕复杂一些,都不会让你觉得别扭。而这在Hibernate中,复杂的hql就会让人感觉有点别扭。

  缺点也很显著:你需要手写SQL,以及定义结果集如何映射到java对象中。手写SQL有时候比较复杂,而定义映射有时候更加复杂,所以怕麻烦的人都倾向于hibernate进行快速开发,至于性能问题,等遇到时再说吧。

  Mybatis提供了一个generator,让我们设计好数据库之后,直接生成对应的java类和配置文件。这个generator是个败笔,因为它生成的代码太复杂了,一个几个字段的数据表,生成了近千行的代码,谁敢用?曾经也是这些代码把我吓退了--难道Mybatis这么复杂?

  好在仔细看过Mybatis的文档和相关的demo,发现其实很简单,关键就在于XML中的SQL和ResultMap的配置。在IDE的帮助下,使用XML并不是一件复杂的事情,但是用XML来描述映射关系,却非常啰嗦,因为XML的表达力比较弱。但是放在java中也不行,因为Java的表达力同样弱,所以最终还是放在XML中。好在现在支持annotation了,可以把一些东西放在java代码里,方便清晰很多。

  我觉得mybatis与play中提供的那个anorm,在原理上非常相似,只是因为anorm使用了scala,利用其强大的表达力,简化了SQL定义以及映射。开发效率提高了很多,代码量少很多,而且还有了编译器的检查。我感觉mybatis的思想,只有在表达力丰富的语言上(如scala),才能得到发扬光大。

  现在遇到的问题是事务。我看的一些例子,都是以编码的方式实现事务,插入数据后,马上要调用session.commit,最后还要session.close。对于测试来说,这有些麻烦。因为这样做,每一个测试运行后,数据库的内容就会变化,对后面的测试产生影响。如果在每个测试运行前,都删除所有数据再插入初始数据,又太费时了。以前在使用hibernate的时候,人们通常使用spring,利用spring的声明性事务,避免在代码中硬编码commit/close,然后,在单元测试中,使用spring提供的一个测试基类,就可以做到在测试完成后,rollback事务,即不会修改数据库内容,又成功的测试了代码。(Spring在易用性方面,的确做得非常好)

  我本想只使用mybatis,不想引入spring,但是mybatis似乎没有考虑到测试的问题。经常多次尝试,发现要想rollback,唯一的方式就是声明性事务,要达到这一点,只能使用一些提供了aop功能的ioc容器,比如spring,guice等。我打算先尝试一下guice,不行的话再使用spring。(为什么先试guice?因为我感觉它比较轻量,spring很庞大)

  原理如下:

  1。声明一个datasource。可采用一些数据库连接池提供的datasource,如今天发现的一个叫bonecp的连接池(当然,dbcp,c3p0,proxool等也可以),生成一个datasource对象。

  2。datasource似乎通常都是以jndi方式提供的,但是实现起来有些麻烦,不知道可否以非jndi的方式实现

  3。在代码中,使用该datasource取connection,进行数据库操作。同时,使用声明性事务,commit/rollback/close都不要出现在代码中。

  4。不论使用guice还是spring,都将以代码方式实现mybatis的配置,而不是用XML。

  5。可能需要实现一个junit的基类(如果是guice的话),实现事务不提交,在测完后自动回滚。

———————————- 接上 ———————————————————-

出门回来后,继续使用mybatis。

对于之前想将mybatis与guice结合起来,做成声明性事务,以便在测试时回滚,经研究后,发现在play的环境下难以实现,并且不太有必要。我之所以希望这样,是因为在测试数据库时,每个测试前都需要手动删除数据,再添加预定义的数据,非常麻烦;如果回滚事务不提交,则可省了这步,因为数据库内容不会变化。但是后来发现,使用dbunit这个东西,可以方便地解决这个问题,而且适用性更广泛,那么就没有必要把事情弄那么复杂。

然后继续写dao。mybatis没有提供让我们自定义pojo字段名与表中列名的对应关系的方法,所以只能在xml中手写映射。我起的名字都是有规则的,比如在pojo中叫createdAt,在数据库中就是created_at,即java中是驼峰式,而数据库中是下划线式,这符合两者的命名习惯。但是mybatis就是不认,也没有提供一个简单的方法让我们配置一下,要写很多繁琐的废话,真麻烦。

体力活干完了,简单的SQL没有问题了,但是很快发现了一个大问题:重复列名如何区分?!

这在hibernate中根本无须考虑的事情,在mybatis中却成了一个麻烦的事情,而且是我完全没有预料到的。假设我有两个表questions和answers,它们都有一些通用的字段,比如id, content, created_at, user_id等等。如果我要写一个SQL,来读取questions以及它的answers,会写成这样:

select q.*, a.* from questions as q left join answers as a on q.id=a.question_id order by q.created_at desc

映射成pojo之后,发现answers中的很多字段都有问题,比如id, content, created_at,都是question的数据。原因是questions和answers中的相同字段名,只有第一个被多次使用,其它的被忽略。

要解决这个问题,我想到两种办法,都不太好。

一是手动将数据库中的字段名改成完全不重复的,所以questions中的id会变为question_id,content会变为question_content,created_at会变为question_created_at,answers和其它表中也这么改。

二是不改数据库,而改sql语句:

select q.id as q_id, q.content as q_content, q.created_at as q_created_at, ..., a.id as a_id, a.content as a_content, a.created_at as
   a_created_at from questions as q left join answers as a on q.id=a.question_id order by q.created_at desc

这样每个类似的SQL都要改,更是麻烦无比。

我想这就是风格问题。从数据库管理员出身的程序员,因为数据库方面的限制,早已养成了自己的习惯:比如使用业务主键(所以各个表的主键名是基本上不同的),每个字段名都是不同的(比如加前缀),避开了字段名重复的问题,且不以此为苦。mybatis延续了这种风格,强调严谨、繁琐,正合他们的口味。而hibernate就会让他们觉得难以理解和使用。

而普通程序员则追求的是代码的简洁、不重复、设计优良,所以倾向于使用非业务主键(基本上都叫id),简洁名称(所以会重复)。Hibernate就是为他们准备的,而mybatis会让他们觉得繁琐、啰嗦。

正是这个问题,直接导致我不想再使用mybatis。为什么hibernate没有这个问题呢?因为hibernate的sql是自动生成的,它可以在生成的字段名前面加一些前缀之类,让每个字段都不重复。

comments powered by Disqus