动态 SQL 通常要做的事情是根据条件包含 where 子句的一部分。比如:
PHP <select> SELECT * FROM BLOG WHERE state = ‘ACTIVE’ <if test="title != nil"> AND title like #{title} </if>
这条语句提供了一种可选的查找文本功能。如果没有传入“title”,那么所有处于“ACTIVE”状态的BLOG都会返回;反之若传入了“title”,那么就会对“title”一列进行模糊查找并返回 BLOG 结果(细心的读者可能会发现,“title”参数值是可以包含一些掩码或通配符的)。
如果希望通过“title”和“author”两个参数进行可选搜索该怎么办呢?首先,改变语句的名称让它更具实际意义;然后只要加入另一个条件即可。
PHP <select> SELECT * FROM BLOG WHERE state = ‘ACTIVE’ <if test="title != nil"> AND title like #{title} </if> <if test="author != nil and author.name != nil"> AND author_name like #{author.name} </if> </select>
有时我们不想应用到所有的条件语句,而只想从中择其一项。针对这种情况,Dynamic Sql 提供了 choose 元素,它有点像编程语言中的 switch 语句。
还是上面的例子,但是这次变为提供了“title”就按“title”查找,提供了“author”就按“author”查找的情形,
若两者都没有提供,就返回所有符合条件的 BLOG(实际情况可能是由管理员按一定策略选出 BLOG 列表,而不是返回大量无意义的随机结果)。
HTML <select> SELECT * FROM BLOG WHERE state = ‘ACTIVE’ <choose> <when test="title != nil"> AND title like #{title} </when> <when test="author != nil and author.name != nil"> AND author_name like #{author.name} </when> <otherwise> AND featured = 1 </otherwise> </choose>
前面几个例子已经合宜地解决了一个臭名昭著的动态 SQL 问题。现在回到“if”示例,这次我们将“ACTIVE = 1”也设置成动态的条件,看看会发生什么。
PHP <select> SELECT * FROM BLOG WHERE <if test="state != nil"> state = #{state} </if> <if test="title != nil"> AND title like #{title} </if> <if test="author != nil and author.name != nil"> AND author_name like #{author.name} </if>
如果这些条件没有一个能匹配上会发生什么?最终这条 SQL 会变成这样:
SELECT * FROM BLOG
这会导致查询失败。如果仅仅第二个条件匹配又会怎样?这条 SQL 最终会是这样:
SELECT * FROM BLOG WHERE
这个查询也会失败。这个问题不能简单地用条件句式来解决,如果你也曾经被迫这样写过,那么你很可能从此以后都不会再写出这种语句了。
Dynamic Sql 有一个简单的处理,这在 90% 的情况下都会有用。而在不能使用的地方,你可以自定义处理方式来令其正常工作。一处简单的修改就能达到目的:
PHP <select> SELECT * FROM BLOG <where> <if test="state != nil"> state = #{state} </if> <if test="title != nil"> AND title like #{title} </if> <if test="author != nil and author.name != nil"> AND author_name like #{author.name} </if> </where>
where 元素只会在至少有一个子元素的条件返回 SQL 子句的情况下才去插入“WHERE”子句。而且,若语句的开头为“AND”或“OR”,where 元素也会将它们去除。
如果 where 元素没有按正常套路出牌,我们可以通过自定义 trim 元素来定制 where 元素的功能。比如,和 where 元素等价的自定义 trim 元素为:
HTML <trim prefix="WHERE" prefixOverrides="AND |OR "> ...
prefixOverrides 属性会忽略通过 | 分隔的文本序列(注意此例中的空格也是必要的)。它的作用是移除所有指定在 prefixOverrides 属性中的内容,并且插入 prefix 属性中指定的内容。
你可以写一些更加复杂的 trim 元素来应对其他情况,比如:
C++ <trim prefix="(" suffix=")" prefixOverrides="," suffixOverrides=","> ...
上面的 trim 元素会尝试去掉结果开头的和结尾多余的 ,,并在开头和结尾加上括号。
动态 SQL 的另外一个常用的操作需求是对一个集合进行遍历,通常是在构建 IN 条件语句的时候。比如:
C# <select> SELECT * FROM POST P WHERE ID in <foreach item="item" index="index" collection="list" open="(" separator="," close=")"> #{item} </foreach>
foreach 元素的功能非常强大,它允许你指定一个集合,声明可以在元素体内使用的集合项(item)和索引(index)变量。它也允许你指定开头与结尾的字符串以及在迭代结果之间放置分隔符。这个元素是很智能的,因此它不会偶然地附加多余的分隔符。
注意 你可以将任何可迭代对象(如 List、Set 等)、Map 对象或者数组对象传递给 foreach 作为集合参数。当使用可迭代对象或者数组时,index 是当前迭代的次数,item 的值是本次迭代获取的元素。当使用 Map 对象时,index 是键,item 是值。
到此我们已经完成了涉及 Dynamic Sql 的讨论。
bind 元素可以根据指定的表达式创建一个变量并将其绑定到上下文。比如:
JavaScript <select> <bind name="pattern" value="'%' + book.title + '%'" /> SELECT * FROM BLOG WHERE title LIKE #{pattern}
function 元素允许你定义一个函数片段,并且该函数可以递归,你可以这样定义一个函数
JavaScript <select> <function name="f1" parameters="dimension,value"> ${dimension} = #{value} </function> SELECT * FROM BLOG WHERE <f1 dimension="title" value="book.title" /> <f1 dimension="author" value="book.author" />
上面的 function 元素中定义了一个函数叫 f1, 并且其拥有两个参数 dimension 和 value, 参数名之间用 , 隔开。
接下来, 我们使用 f1 元素来调用函数,并通过元素的属性 dimension 和 value 给函数传递参数
function 元素可以和 foreach 配合使用来实现十分强大的功能。并且 function 允许递归调用,比如你可以这样来递归调用:
C <select> <function name="f1" parameters="dimension,value"> <if test="dimension != nil"> ${dimension} = #{value} </if> <if test="dimension == nil"> <f1 dimension="value.first" value="value.second" /> </if </function> SELECT * FROM BLOG WHERE <f1 dimension="title" value="book.title" /> <f1 dimension="nil" value="book.filter" />
上面的 function 元素中,通过递归调用完成进一步的解析,递归调用使得解析一些嵌套逻辑变得十分简单,比如实现多层的 AND OR 的逻辑嵌套。你也可以通过定义只返回固定 SQL 片段的常函数,这和C语言的宏定义很像。
Dynamic Sql 会对 if、choose里的 test 语句, foreach 里的 collection 语句, bind 里的 value 语句, 以及 function 的参数进行表达式求值。
除了会对上诉的元素的个别属性的表达式进行求值以外,还会对 ${} 和 #{} 里包含的表达式求值,两者的不同点是后者会转成成类似于 prepare statement 里的 SQL 参数, 类似于 select * from BLOG where title = ? 里的 ?,而前者会直接拼接到 SQL 中,所以应当尽量使用 #{}, 因为这可以有效防止 SQL 注入问题。