title: 动态SQL---MyBatis date: 2017.06.10 19:18 categories:
- 技术博客 tags:
- 框架了解
- MyBatis
能够体现MyBatis这个框架的强大与灵活特性的一点,就是Dynamic SQL,正常且合理来讲,学习持久层框架前,Java程序员应该自己去尝试实现JDBC,从耦合到解耦,从不可复用到通用,这个过程不仅仅可以理解JPA规范,更能让自己的基础更扎实,对简单的设计模式也会加入实践。
经过自己对JDBC的折腾,我的一个感触就是:综合来看就一点最关键,即拼接SQL语句,这其实也是代码所要解决最麻烦的问题。
MyBatis的XML解析方案一方面写起来代码不耦合,另一方面用起来功能灵活,并且与Hibernate很不同的一点就是:适合开发者直接对SQL调优。从自己尝试的过程中也有所体现,其实大家都在朝着这个方向编码。
1. 大概原理
无非就是XML解析。
用过JSP的人一定用过JSTL,同理,MyBatis也是定义了自己的标签库,通过标签建立ORM这种思想上来源于OGNL,不过这不是今天的重点,大概明白他是怎么做的就行了。
MyBatis3后的标签精简到了四种:
- if
- choose(when,otherwise)
- trim(where,set)
- foreach
2. 标签用法
- if
写where语句是最常见的:
<select id="findActiveBlogWithTitleLike"
resultType="Blog">
SELECT * FROM BLOG
WHERE state = ‘ACTIVE’
<if test="title != null">
AND title like #{title}
</if>
</select>
这个场景里面,用户可输入想要的博客的名称,不输入的话到了拼接SQL这层上会自动去掉后面的AND title like #{title},这归功于if,里面的test写的就是条件。当然,现实情况中我们可能会有多个条件,例如:
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</select>
相应的在用户界面上就多了个作者的输入口,if标签简单实用。
- choose, when, otherwise
有些时候我们会有类似于Java中switch的需求,这时候单单if可能就不合适了,,因为if是多选,而switch是单选,而choose, when, otherwise的组合可以实现Java中switch的效果:
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<choose>
<when test="title != null">
AND title like #{title}
</when>
<when test="author != null and author.name != null">
AND author_name like #{author.name}
</when>
<otherwise>
AND featured = 1
</otherwise>
</choose>
</select>
上面的otherwise也类似于Javaswitch中的default,即前面两个条件都不满足的情况下才返回featured=1的数据。
- trim, where, set
前面的几个标签已足以应付大多数的场景,但前面有个一定成立的条件state = ‘ACTIVE’,所有条件都动态后就尴尬了:
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG
WHERE
<if test="state != null">
state = #{state}
</if>
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</select>
如果所有条件都没满足,拼出来的SQL就长这样:
SELECT * FROM BLOG
WHERE
这样当然不行,接下来还有第二个条件被满足的情况:
SELECT * FROM BLOG
WHERE
AND title like ‘someTitle’
这样当然也是不行的,所以多条件的拼接处理起来有些麻烦,但MyBatis的用法真叫一个优雅,因为这些问题已经得到了完美解决,只需要在前面的基础上添加一个where闭合标签:
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG
<where>
<if test="state != null">
state = #{state}
</if>
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</where>
</select>
这个标签做到了智能检测何时插入where,并且后面的and也是根据条件来拼接的,官方宣称可以应付90%的场景,当然我平时写下来用这个标签是最多的,一直完美运行。
除了where,MyBatis还给了开发者自定义的选项,即trim,你可以理解为这是where的详细定义版,我们可以配置具体的元素,下面的代码与上面是一样的功能:
<trim prefix="WHERE" prefixOverrides="AND |OR ">
...
</trim>
prefix代表前缀,内部的条件只要有满足的就拼上,反之就不拼,当然suffix后缀功能也有,而prefixOverrides则负责检测是否需要删掉前缀(后缀同理),如果需要配置多个项需要遵循两个规则:
- 不同项使用
|进行分隔(关于pipe delimited的简单认识) |前面还有一个空格(如上述代码中的and后有个空格)
同样地,在update中我们使用set的时候也有类似的情况:
<update id="updateAuthorIfNecessary">
update Author
<set>
<if test="username != null">username=#{username},</if>
<if test="password != null">password=#{password},</if>
<if test="email != null">email=#{email},</if>
<if test="bio != null">bio=#{bio}</if>
</set>
where id=#{id}
</update>
而使用trim后我们就可以自定义前缀set,以及前后缀复写(智能删除最后一项的标记):
<trim prefix="SET" suffixOverrides=",">
...
</trim>
我同事写过稍微详细点的:
<insert id="insertSelective" >
INSERT INTO tableName
<trim prefix="(" suffix=")" suffixOverrides="," >
<if test="column_first != null" >
COLUMN_FIRST,
</if>
<if test="columnSecond!= null" >
COLUMN_SECOND,
</if>
</trim>
</insert>
一方面必须承认他这个代码有一些冗余(高级黑),但这里我们只是拿来看看如何配置详细的前后缀,明白意思即可。
- foreach
在迭代集合的情况下,foreach就成为了首选,直接放代码:
<select id="selectPostIn" resultType="domain.blog.Post">
SELECT *
FROM POST P
WHERE ID in
<foreach item="item" index="index" collection="list"
open="(" separator="," close=")">
#{item}
</foreach>
</select>
collection代表你将要迭代的集合,index代表元素索引(根据你的对象类型有所区分),item代表每项元素,同时我们还可以自定义开闭以及分隔符号。
**注意:**可以迭代多种类型(List、Set等),包括Map以及数组对象都是可以的,特别需要留意的一点,迭代Map时,index代表键对象,而item代表值对象。
- bind
bind标签我还真没在项目里面用过,不过也很简单,这个标签支持我们在XML中定义自己的变量,比如模糊查询中定义模糊字段:
<select id="selectBlogsLike" resultType="Blog">
<bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
SELECT * FROM BLOG
WHERE title LIKE #{pattern}
</select>
- Multi-db vendor support
可配置的多数据库支持,我们可以通过预先定义每个数据库的id来在SQL XML中动态选择语句:
<insert id="insert">
<selectKey keyProperty="id" resultType="int" order="BEFORE">
<if test="_databaseId == 'oracle'">
select seq_users.nextval from dual
</if>
<if test="_databaseId == 'db2'">
select nextval for seq_users from sysibm.sysdummy1"
</if>
</selectKey>
insert into users values (#{id}, #{name})
</insert>
- Pluggable Scripting Languages For Dynamic SQL
在MyBatis3.2版本中提供了插件式脚本的功能,也就是说我们可以自定义语句拼接的规则:
首先实现以下接口:
public interface LanguageDriver {
ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql);
SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType);
SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType);
}
其次在配置中修改默认的脚本驱动:
<typeAliases>
<typeAlias type="org.sample.MyLanguageDriver" alias="myLanguage"/>
</typeAliases>
<settings>
<setting name="defaultScriptingLanguage" value="myLanguage"/>
</settings>
在不改变默认的情况下,我们可以针对特定的语句应用自己的脚本,只需要在语句上添加lang标签:
<select id="selectBlog" lang="myLanguage">
SELECT * FROM BLOG
</select>
如果使用注解的情况,框架也提供了@Lang,非常方便:
public interface Mapper {
@Lang(MyLanguageDriver.class)
@Select("SELECT * FROM BLOG")
List<Blog> selectBlog();
}
官方推荐使用Apache Velocity来做动态页面,我这里用的是FreeMarker,道理上是一样的,总之要结合实践嘛。
**注意:**官方的脚本驱动写在了org.apache.ibatis.scripting.xmltags.XmlLanguageDriver中,默认的别名是xml。
3. 小结
简单介绍了下MyBatis中的动态SQL,从项目的角度来看,可能具体的标签用法更加重要,但从我个人学习的角度来看,框架的设计理念更值得琢磨,实践确实是检验框架的真理,多用多学吧。