这篇文章资料来自于网络,对部分知识整理,这里只是记录一下,仅供参考。
动态 SQL
动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。
使用动态 SQL 并非一件易事,但借助可用于任何 SQL 映射语句中的强大的动态 SQL 语言,MyBatis 显著地提升了这一特性的易用性。
如果你之前用过 JSTL 或任何基于类 XML 语言的文本处理器,你对动态 SQL 元素可能会感觉似曾相识。在 MyBatis 之前的版本中,需要花时间了解大量的元素。借助功能强大的基于 OGNL 的表达式,MyBatis 3 替换了之前的大部分元素,大大精简了元素种类,现在要学习的元素种类比原来的一半还要少。
- if
- choose (when, otherwise)
- trim (where, set)
- foreach
if
使用动态 SQL 最常见情景是根据条件包含 where 子句的一部分。比如:
<select id="findActiveBlogWithTitleLike"
resultType="Blog">
SELECT * FROM BLOG
WHERE state = ‘ACTIVE’
<if test="title != null">
AND title like #{title}
</if>
</select>
这条语句提供了可选的查找文本功能。如果不传入 “title”,那么所有处于 “ACTIVE” 状态的 BLOG 都会返回;如果传入了 “title” 参数,那么就会对 “title” 一列进行模糊查找并返回对应的 BLOG 结果(细心的读者可能会发现,“title” 的参数值需要包含查找掩码或通配符字符)。
如果希望通过 “title” 和 “author” 两个参数进行可选搜索该怎么办呢?首先,我想先将语句名称修改成更名副其实的名称;接下来,只需要加入另一个条件即可。
<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 test="seat_no != null and seat_no != '' ">
AND seat_no = #{seat_no}
</if>
<sql id="search">
<if test="userParams.adminType==null or userParams.adminType==''">
and tu.ADMIN_TYPE_ID in(0,1)
</if>
<if test="userParams.adminType != null and userParams.adminType != ''">
and tu.ADMIN_TYPE_ID=#{userParams.adminType}
</if>
<if test="userParams.roleId != null and userParams.roleId != ''">
and (select group_concat(ur.ROLE_ID)
from t_user u
right join t_user_role ur on ur.USER_ID = u.USER_ID,
t_role r
where r.ROLE_ID = ur.ROLE_ID
and u.USER_ID = tu.USER_ID and r.ROLE_ID=#{userParams.roleId})
</if>
<if test="userParams.mobile != null and userParams.mobile != ''">
AND tu.MOBILE =#{userParams.mobile}
</if>
<if test="userParams.username != null and userParams.username != ''">
AND tu.USERNAME like CONCAT('%',#{userParams.username},'%')
</if>
<if test="userParams.ssex != null and userParams.ssex != ''">
AND tu.SSEX =#{userParams.ssex}
</if>
<if test="userParams.status != null and userParams.status != ''">
AND tu.STATUS =#{userParams.status}
</if>
<if test="userParams.deptId != null and userParams.deptId != ''">
AND td.DEPT_ID =#{userParams.deptId}
</if>
<if test="userParams.createTime != null and userParams.createTime != ''">
AND DATE_FORMAT(tu.CREATE_TIME,'%Y%m%d') BETWEEN substring_index(#{userParams.createTime},'#',1) and substring_index(#{userParams.createTime},'#',-1)
</if>
</sql>
choose、when、otherwise
有时候,我们不想使用所有的条件,而只是想从多个条件中选择一个使用。针对这种情况,MyBatis 提供了 choose 元素,它有点像 Java 中的 switch 语句。
还是上面的例子,但是策略变为:传入了 “title” 就按 “title” 查找,传入了 “author” 就按 “author” 查找的情形。若两者都没有传入,就返回标记为 featured 的 BLOG(这可能是管理员认为,与其返回大量的无意义随机 Blog,还不如返回一些由管理员精选的 Blog)。
<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>
<choose>
<when test="……">
……
</when>
<otherwise>
……
</otherwise>
</choose>
<select id="findUsersByUser" resultType="cn.soboys.kmall.sys.entity.User">
select tu.USER_ID,tu.USERNAME,tu.SSEX,td.DEPT_NAME,tu.MOBILE,tu.EMAIL,tu.STATUS,tu.CREATE_TIME,
td.DEPT_ID
from t_user tu left join t_dept td on tu.DEPT_ID = td.DEPT_ID
<where>
<choose>
<when test="userParams.adminType==4">
and tu.ADMIN_TYPE_ID in(2,3)
</when>
<otherwise>
<include refid="search"></include>
</otherwise>
</choose>
</where>
</select>
trim、where、set
前面几个例子已经方便地解决了一个臭名昭著的动态 SQL 问题。现在回到之前的 “if” 示例,这次我们将 “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
这会导致查询失败。如果匹配的只是第二个条件又会怎样?这条 SQL 会是这样:
SELECT * FROM BLOG
WHERE
AND title like ‘someTitle’
这个查询也会失败。这个问题不能简单地用条件元素来解决。这个问题是如此的难以解决,以至于解决过的人不会再想碰到这种问题。
MyBatis 有一个简单且适合大多数场景的解决办法。而在其他场景中,可以对其进行自定义以符合需求。而这,只需要一处简单的改动:
<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 元素只会在子元素返回任何内容的情况下才插入 “WHERE” 子句。而且,若子句的开头为 “AND” 或 “OR”,where 元素也会将它们去除。
如果 where 元素与你期望的不太一样,你也可以通过自定义 trim 元素来定制 where 元素的功能。比如,和 where 元素等价的自定义 trim 元素为:
<trim prefix="WHERE" prefixOverrides="AND |OR ">
...
</trim>
prefixOverrides 属性会忽略通过管道符分隔的文本序列(注意此例中的空格是必要的)。上述例子会移除所有 prefixOverrides 属性中指定的内容,并且插入 prefix 属性中指定的内容。
用于动态更新语句的类似解决方案叫做 set。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>
这个例子中,set 元素会动态地在行首插入 SET 关键字,并会删掉额外的逗号(这些逗号是在使用条件语句给列赋值时引入的)。
或者,你可以通过使用trim元素来达到同样的效果:
<trim prefix="SET" suffixOverrides=",">
...
</trim>
注意,我们覆盖了后缀值设置,并且自定义了前缀值。
foreach
动态 SQL 的另一个常见使用场景是对集合进行遍历(尤其是在构建 IN 条件语句的时候)。比如:
<select id="selectPostIn" resultType="domain.blog.Post">
SELECT *
FROM POST P
<where>
<foreach item="item" index="index" collection="list"
open="ID in (" separator="," close=")" nullable="true">
#{item}
</foreach>
</where>
</select>
foreach 元素的功能非常强大,它允许你指定一个集合,声明可以在元素体内使用的集合项(item)和索引(index)变量。它也允许你指定开头与结尾的字符串以及集合项迭代之间的分隔符。这个元素也不会错误地添加多余的分隔符,看它多智能!
提示 你可以将任何可迭代对象(如 List、Set 等)、Map 对象或者数组对象作为集合参数传递给 foreach。当使用可迭代对象或者数组时,index 是当前迭代的序号,item 的值是本次迭代获取到的元素。当使用 Map 对象(或者 Map.Entry 对象的集合)时,index 是键,item 是值。
至此,我们已经完成了与 XML 配置及映射文件相关的讨论。下一章将详细探讨 Java API,以便你能充分利用已经创建的映射配置。
script
要在带注解的映射器接口类中使用动态 SQL,可以使用 script 元素。比如:
@Update({"<script>",
"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}",
"</script>"})
void updateAuthorValues(Author author);
bind
bind
元素允许你在 OGNL 表达式以外创建一个变量,并将其绑定到当前的上下文。比如:
<select id="selectBlogsLike" resultType="Blog">
<bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
SELECT * FROM BLOG
WHERE title LIKE #{pattern}
</select>
多数据库支持
如果配置了 databaseIdProvider,你就可以在动态代码中使用名为 “_databaseId” 的变量来为不同的数据库构建特定的语句。比如下面的例子:
<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>
动态 SQL 中的插入脚本语言
MyBatis 从 3.2 版本开始支持插入脚本语言,这允许你插入一种语言驱动,并基于这种语言来编写动态 SQL 查询语句。
可以通过实现以下接口来插入一种语言:
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);
}
实现自定义语言驱动后,你就可以在 mybatis-config.xml 文件中将它设置为默认语言:
<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>
或者,在你的 mapper 接口上添加 @Lang
注解:
public interface Mapper {
@Lang(MyLanguageDriver.class)
@Select("SELECT * FROM BLOG")
List<Blog> selectBlog();
}
SQL片段拼接#
我们再写sql语句的时候往往会有这样一些要求,一些重复的sql语句片段,我们不想重复去写,那么可以通过sql片段方式去抽离,公共sql然后在需要的地方去引用
MyBatis
中 <sql>
元素用于定义一个 SQL
片段,用于分离一些公共的 SQL 语句,例如:SELECT
关键字和 WHERE
关键字之间的部分。其中:
id
:唯一标识符,用于在其他地方使用<include>
标签引用;lang:
设置字符编码;databaseId
:指定执行该 SQL 语句的数据库ID,数据库ID在 mybatis-cfg.xml 中的 中配置。
同时,你也能够看见 <sql>
标签中可以使用<include>、<trim>、<where>、<set>、<foreach>、<choose>、<if>、<bind>
等标签定义复杂的 SQL 片段
简单使用定义sql片段如下:
<sql id="user_columns">
`user_id`, `name`, `sex`, `age`
</sql>
在 <sql>
标签中使用<include>
标签引入定义的sql片段,如下:
<!-- 定义基础列 -->
<sql id="user_base_columns">
`user_id`, `name`
</sql>
<!-- 定义一个SQL片段 -->
<sql id="user_columns">
<include refid="user_base_columns"/>, `sex`, `age`
</sql>
场景使用案例如:查询用户信息
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.hxstrive.mybatis.sql.demo1.UserMapper">
<!-- 映射结果 -->
<resultMap id="RESULT_MAP" type="com.hxstrive.mybatis.sql.demo1.UserBean">
<id column="user_id" jdbcType="INTEGER" property="userId" />
<result column="name" jdbcType="VARCHAR" property="name" />
<result column="sex" jdbcType="VARCHAR" property="sex" />
<result column="age" jdbcType="INTEGER" property="age" />
</resultMap>
<!-- 定义一个SQL片段 -->
<sql id="user_columns">
`user_id`, `name`, `sex`, `age`
</sql>
<!-- 查询所有用户信息 -->
<select id="findAll" resultMap="RESULT_MAP">
select <include refid="user_columns" /> from `user`
</select>
</mapper>
SQL参数取值和OGNL表达式#
看到我们上面去值参数通过#{params}
这种方式来去值的其中传进来的参数 #{xx} 就是使用的 OGNL
表达式。
Mybatis 官方文档中「XML 映射文件」模块里边,有解析到:
说当我们使用 #{} 类型参数符号的时候,其实就是告诉 Mybatis 创建一个预处理语句参数,通过 JDBC,这样的一个参数在 SQL 中会由一个 "?" 来标识,并传递到一个新的预处理语句中。
也就是说当我们使用 #{XX} OGNL 表达式的时候, 它会先帮我们生成一条带占位符的 SQL 语句,然后在底层帮我们设置这个参数:ps.setInt(1, id);
OGNL 是 Object-Graph Navigation Language 的缩写,对象-图行导航语言,语法为:#{ }。
是不是有点懵,不知道这是个啥?
OGNL 作用是在对象和视图之间做数据的交互,可以存取对象的属性和调用对象的方法,通过表达式可以迭代出整个对象的结构图
MyBatis常用OGNL表达式如下:
上述内容只是合适在MyBatis中使用的OGNL表达式,完整的表达式点击这里。
MyBatis中可以使用OGNL的地方有两处:
- 动态
SQL
表达式中 ${param}
参数中
如下例子MySql like 查询:
<select id="xxx" ...>
select id,name,... from country
<where>
<if test="name != null and name != ''">
name like concat('%', #{name}, '%')
</if>
</where>
</select>
上面代码中test的值会使用OGNL计算结果。
例二,通用 like 查询:
<select id="xxx" ...>
select id,name,... from country
<bind name="nameLike" value="'%' + name + '%'"/>
<where>
<if test="name != null and name != ''">
name like #{nameLike}
</if>
</where>
</select>
这里的value值会使用OGNL计算。
注:对<bind参数的调用可以通过#{}或 ${} 方式获取,#{}可以防止注入。
在通用Mapper中支持一种UUID的主键,在通用Mapper中的实现就是使用了标签,这个标签调用了一个静态方法,大概方法如下:
<bind name="username_bind"
value='@java.util.UUID@randomUUID().toString().replace("-", "")' />
这种方式虽然能自动调用静态方法,但是没法回写对应的属性值,因此使用时需要注意。
${params}
中的参数
上面like的例子中使用下面这种方式最简单
<select id="xxx" ...>
select id,name,... from country
<where>
<if test="name != null and name != ''">
name like '${'%' + name + '%'}'
</if>
</where>
</select>
这里注意写的是${'%' + name + '%'},
而不是%${name}%,
这两种方式的结果一样,但是处理过程不一样。
在MyBatis
中处理${}
的时候,只是使用OGNL
计算这个结果值,然后替换SQL中对应的${xxx}
,OGNL处理的只是${这里的表达式}。
这里表达式可以是OGNL支持的所有表达式,可以写的很复杂,可以调用静态方法返回值,也可以调用静态的属性值。
例子,条件判断入参属性值是否包含子字符串可以直接使用 contains
判断
<foreach collection="list" item="item" index="index" separator="AND" open="(" close=")">
<choose>
<when test='item.cname.contains("select") or item.cname.contains("checkbox") or item.cname.contains("date")'>
<if test='item.cname.contains("select") or item.cname.contains("checkbox")'>
find_in_set(#{item.value},base.${item.cname})
</if>
<if test='item.cname.contains("date")'>
DATE_FORMAT(base.${item.cname},'%Y-%m-%d') = DATE_FORMAT(#{item.value},'%Y-%m-%d')
</if>
</when>
<otherwise>
base.${item.cname} = #{item.value}
</otherwise>
</choose>
</foreach>
参考
https://mybatis.org/mybatis-3/zh_CN/dynamic-sql.html
MyBatis系列(六):MyBatis动态Sql之if标签的用法1. 使用if标签实现动态查询 假设有这样1个需求:根 - 掘金