原副标题:该文掌控 MyBatis 的静态 SQL 采用与基本原理
译者 | 宏碁云开发人员国联
全文:采用静态 SQL 绝非两件难事,但借助于可用作任何人 SQL 态射句子中的强悍的静态 SQL 词汇,MyBatis 明显地提高了这一优点的可用性。
责任编辑撷取自宏碁云街道社区《MyBatis 简述 – 静态 SQL 采用与基本原理》,译者:小弟博纳县 。
静态 SQL 是 MyBatis 的强悍优点众所周知。假如你采所用 JDBC 或其他类似于的架构,你如果能认知依照相同前提堆叠 SQL 句子有多伤痛,比如堆叠时要保证无法忘掉加进必要性的字符,更要特别注意拿掉条目最终两个入选为的双引号。借助静态 SQL,能彻底全盘摆脱此种伤痛。
采用静态 SQL 绝非两件难事,但借助于可用作任何人 SQL 态射句子中的强悍的静态 SQL 词汇,MyBatis 明显地提高了这一优点的可用性。
假如你以后所用 JSTL 或任何人如前所述类 XML 词汇的文档CPU,你对静态 SQL 原素可能会觉得有种。在 MyBatis 以后的版中,须要花天数介绍大批的原素。借助于功能强悍的如前所述 OGNL 的函数,MyBatis 3 代替了以后的大部分原素,大幅简化了原素类型,那时要自学的原素类型比原本的三分众所周知更要少。
if choose (when, otherwise) trim (where, set) foreachif采用静态 SQL 最常用情境是依照前提包涵 where 从句的一小部分。比如说:
< selectid= “findActiveBlogWithTitleLike”
resultType= “Blog”>
SELECT * FROM BLOG
WHERE state = ‘ACTIVE’
< iftest= “title != null”>
AND title like #{title}
</ if>
</ select>
这条句子提供了可选的查找文档功能。假如不传入 “title”,那么所有处于 “ACTIVE” 状态的 BLOG 都会返回;假如传入了 “title” 参数,那么就会对 “title” 一列进行模糊查找并返回对应的 BLOG 结果(细心的读者可能会发现,“title” 的参数值须要包涵查找掩码或通配符字符)。
假如希望通过 “title” 和 “author” 两个参数进行可选搜索该怎么办呢?首先,我想先将句子名称修改成更名副其实的名称;接下来,只须要加入另两个前提即可。
< selectid= “findActiveBlogLike”
resultType= “Blog”>
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
< iftest= “title != null”>
AND title like #{title}
</ if>
< iftest= “author != null and author.name != null”>
AND author_name like #{author.name}
</ if>
</ select> choose、when、otherwise
有时候,我们不想采用所有的前提,而只是想从多个前提中选择两个采用。针对此种情况,MyBatis 提供了 choose 原素,它有点像 Java 中的 switch 句子。
还是上面的例子,但是策略变为:传入了 “title” 就按 “title” 查找,传入了 “author” 就按 “author” 查找的情形。若两者都没有传入,就返回标记为 featured 的 BLOG(这可能是管理员认为,与其返回大批的无意义随机 Blog,还不如返回一些由管理员挑选的 Blog)。
< selectid= “findActiveBlogLike”
resultType= “Blog”>
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
< choose>
< whentest= “title != null”>
AND title like #{title}
</ when>
< whentest= “author != null and author.name != null”>
AND author_name like #{author.name}
</ when>
< otherwise>
AND featured = 1
</ otherwise>
</ choose>
</ select>
trim、where、set
前面几个例子已经合宜地解决了两个臭名昭著的静态 SQL 问题。那时回到以后的 “if” 示例,这次我们将 “state = ‘ACTIVE’” 设置成静态前提,看看会发生什么。
< selectid= “findActiveBlogLike”
resultType= “Blog”>
SELECT * FROM BLOG
WHERE
< iftest= “state != null”>
state = #{state}
</ if>
< iftest= “title != null”>
AND title like #{title}
</ if>
< iftest= “author != null and author.name != null”>
AND author_name like #{author.name}
</ if>
</ select>
假如没有匹配的前提会怎么样?最终这条 SQL 会变成这样:
SELECT* FROMBLOG
WHERE
这会导致查询失败。假如匹配的只是第二个前提又会怎样?这条 SQL 会是这样:
SELECT* FROMBLOG
WHERE
ANDtitle like‘someTitle’
这个查询也会失败。这个问题无法简单地用前提原素来解决。这个问题是如此的难以解决,以至于解决过的人不会再想碰到此种问题。
MyBatis 有两个简单且适合大多数场景的解决办法。而在其他场景中,能对其进行自定义以符合需求。而这,只须要一处简单的改动:
< selectid= “findActiveBlogLike”
resultType= “Blog”>
SELECT * FROM BLOG
< where>
< iftest= “state != null”>
state = #{state}
</ if>
< iftest= “title != null”>
AND title like #{title}
</ if>
< iftest= “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 原素为:
< trimprefix= “WHERE”prefixOverrides= “AND |OR “>
…
</ trim>
prefixOverrides 属性会忽略通过管道符分隔的文档序列(特别注意此例中的字符是必要性的)。上述例子会移除所有 prefixOverrides 属性中指定的内容,并且插入 prefix 属性中指定的内容。
用作静态更新句子的类似于解决方案叫做 set。set 原素能用作静态包涵须要更新的列,忽略其它不更新的列。比如说:
<update id= “updateAuthorIfNecessary”>
update Author
< set>
< iftest= “username != null”>username=#{username},</if>
< iftest= “password != null”>password= #{password},</if>
< iftest= “email != null”>email=#{email},</if>
< iftest= “bio != null”>bio= #{bio}</if>
</ set>
whereid= #{id}
</update>
这个例子中,set 原素会静态地在行首插入 SET 关键字,并会删掉额外的双引号(这些双引号是在采用前提句子给列赋值时引入的)。
来看看与 set 原素等价的自定义 trim 原素吧:
< trimprefix= “SET”suffixOverrides= “,”>
…
</ trim>
特别注意,我们覆盖了后缀值设置,并且自定义了前缀值。
foreach
静态 SQL 的另两个常用采用场景是对集合进行遍历(尤其是在构建 IN 前提句子的时候)。比如说:
< selectid= “selectPostIn”resultType= “domain.blog.Post”>
SELECT *
FROM POST P
WHERE ID in
< foreachitem= “item”index= “index”collection= “list”
open= “(“separator= “,”close= “)”>
#{item}
< /foreach>
</select >
foreach 原素的功能非常强悍,它允许你指定两个集合,声明能在原素体内采用的集合项(item)和索引(index)变量。它也允许你指定开头与结尾的字符串以及集合项迭代之间的分隔符。这个原素也不会错误地加进多余的分隔符,看它多智能!
提示:你能将任何人可迭代对象(如 List、Set 等)、Map 对象或者数组对象作为集合参数传递给 foreach。当采用可迭代对象或者数组时,index 是当前迭代的序号,item 的值是至此,我们已经完成了与 XML 配置及态射文件相关的讨论。下一章将详细探讨 Java API,以便你能充分借助已经创建的态射配置。
要在带注解的态射器接口类中采用静态 SQL,能采用 原素。比如说:
@Update({” < >”,
“update Author”,
” < set> “,
” < iftest= username != null> username=#{username}, </ if> “,
” < iftest= password != null> password=#{password}, </if> “,
” < iftest= email != null> email=#{email}, </ if> “,
” < iftest= bio != null> bio=#{bio} </ if> “,
” </ set> “,
“where id=#{id}”,
“</ > “})
void updateAuthorValues(Author author);
bind
bind 原素允许你在 OGNL 函数以外创建两个变量,并将其绑定到当前的上下文。比如说:
< selectid= “selectBlogsLike”resultType= “Blog”>
<bind name= “pattern”value= “% + _parameter.getTitle + %”/>
SELECT * FROM BLOG
WHERE title LIKE #{pattern}
</ select>
多数据库支持
假如配置了 databaseIdProvider,你就可以在静态代码中采用名为 “_databaseId” 的变量来为相同的数据库构建特定的句子。比如说下面的例子:
< insertid= “insert”>
< selectKeykeyProperty= “id”resultType= “int”order= “BEFORE”>
< iftest= “_databaseId == oracle”>
select seq_users.nextval from dual
</ if>
< iftest= “_databaseId == db2”>
select nextval for seq_users from sysibm.sysdummy1″
</ if>
</ selectKey>
insert into users values (#{id}, #{name})
</ insert>
静态 SQL 中的插入脚本词汇
MyBatis 从 3.2 版开始支持插入脚本词汇,这允许你插入一种词汇驱动,并如前所述此种词汇来编写静态 SQL 查询句子。
能通过实现以下接口来插入一种词汇:
publicinterfaceLanguageDriver{
ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql);
SqlSource createSqlSource(Configuration configuration, XNode , Class<?> parameterType);
SqlSource createSqlSource(Configuration configuration, String , Class<?> parameterType);
}
实现自定义词汇驱动后,你就能在 mybatis-config.xml 文件中将它设置为默认词汇:
< typeAliases>
< typeAliastype= “org.sample.MyLanguageDriver”alias= “myLanguage”/>
</ typeAliases>
< settings>
< settingname= “defaultingLanguage”value= “myLanguage”/>
</ settings>
或者,你也能采用 lang 属性为特定的句子指定词汇:
< selectid= “selectBlog”lang= “myLanguage”>
SELECT * FROM BLOG
</ select>
或者,在你的 mapper 接口上加进 @Lang 注解:
publicinterfaceMapper{
@Lang(MyLanguageDriver.class)
@Select( “SELECT * FROM BLOG”)
List<Blog> selectBlog;
}
提示:能采用 Apache Velocity 作为静态词汇,更多细节请参考 MyBatis-Velocity 项目。你前面看到的所有 xml 标签都由默认 MyBatis 词汇提供,而它由词汇驱动 org.apache.ibatis.ing.xmltags.XmlLanguageDriver(别名为 xml)所提供。
静态 SQL 解析基本原理
我们在采用 mybatis 的时候,会在 xml 中编写 sql 句子。比如说这段静态 sql 代码:
< updateid= “update”parameterType= “org.format.dynamicproxy.mybatis.bean.User”>
UPDATE users
< trimprefix= “SET”prefixOverrides= “,”>
< iftest= “name != null and name != “>
name = #{name}
</if>
< iftest= “age != null and age != “>
, age = #{age}
</ if>
< iftest= “birthday != null and birthday != “>
, birthday = #{birthday}
</ if>
</ trim>
where id = ${id}
</ update>
mybatis 底层是如何构造这段 sql 的?下面带着这个疑问,我们一步一步分析。
关于静态 SQL 的接口和类
SqlNode 接口,简单认知就是 xml 中的每个标签,比如说上述 sql 的 update,trim,if 标签:
publicinterfaceSqlNode{
booleanapply(DynamicContext context);
}
SqlSource Sql 源接口,代表从 xml 文件或注解态射的 sql 内容,主要就是用作创建 BoundSql,有实现类 DynamicSqlSource (静态 Sql 源),StaticSqlSource (静态 Sql 源) 等:
publicinterfaceSqlSource{
BoundSql getBoundSql(Object parameterObject);
}
BoundSql 类,封装 mybatis 最终产生 sql 的类,包括 sql 句子,参数,参数源数据等参数:
XNode,两个 Dom API 中的 Node 接口的扩展类:
BaseBuilder 接口及其实现类 (属性,方法省略了,大家有兴趣的自己看), 这些 Builder 的作用就是用作构造 sql:
下面我们简单分析下其中 4 个 Builder:
XMLConfigBuilder :解析 mybatis 中 configLocation 属性中的全局 xml 文件,内部会采用 XMLMapperBuilder 解析各个 xml 文件。 XMLMapperBuilder :遍历 mybatis 中 mapperLocations 属性中的 xml 文件中每个节点的 Builder,比如说 user.xml,内部会采用 XMLStatementBuilder 处理 xml 中的每个节点。 XMLStatementBuilder :解析 xml 文件中各个节点,比如说 select,insert,update,delete 节点,内部会采用 XMLBuilder 处理节点的 sql 部分,遍历产生的数据会丢到 Configuration 的 mappedStatements 中。 XMLBuilder :解析 xml 中各个节点 sql 部分的 Builder。LanguageDriver 接口及其实现类 (属性,方法省略了,大家有兴趣的自己看),该接口主要的作用就是构造 sql:
简单分析下 XMLLanguageDriver (处理 xml 中的 sql,RawLanguageDriver 处理静态 sql):XMLLanguageDriver 内部会采用 XMLBuilder 解析 xml 中的 sql 部分。
源码分析走起
Spring 与 Mybatis 整合的时候须要配置 SqlSessionFactoryBean,该配置会加入数据源和 mybatis xml 配置文件路径等信息:
< beanid= “sqlSessionFactory”class= “org.mybatis.spring.SqlSessionFactoryBean”>
< propertyname= “dataSource”ref= “dataSource”/>
< propertyname= “configLocation”value= “classpath:mybatisConfig.xml”/>
< propertyname= “mapperLocations”value= “classpath*:org/format/dao/*.xml”/>
</ bean>
我们就分析这一段配置背后的细节:
SqlSessionFactoryBean 实现了 Spring 的 InitializingBean 接口,InitializingBean 接口的 afterPropertiesSet 方法中会调用 buildSqlSessionFactory 方法 该方法内部会采用 XMLConfigBuilder 解析属性 configLocation 中配置的路径,还会采用 XMLMapperBuilder 属性解析 mapperLocations 属性中的各个 xml 文件。部分源码如下:
由于 XMLConfigBuilder 内部也是采用 XMLMapperBuilder,我们就看看 XMLMapperBuilder 的解析细节:
我们关注一下,增删改查节点的解析:
XMLStatementBuilder 的解析:
默认会采用 XMLLanguageDriver 创建 SqlSource(Configuration 构造函数中设置)。
XMLLanguageDriver 创建 SqlSource:
XMLBuilder 解析 sql:
得到 SqlSource 之后,会放到 Configuration 中,有了 SqlSource,就能拿 BoundSql 了,BoundSql 能得到最终的 sql。
实例分析
以下面的 xml 解析大概说下 parseDynamicTags 的解析过程:
< updateid= “update”parameterType= “org.format.dynamicproxy.mybatis.bean.User”>
UPDATE users
< trimprefix= “SET”prefixOverrides= “,”>
< iftest= “name != null and name != “>
name = #{name}
</ if>
< iftest= “age != null and age != “>
, age = #{age}
</ if>
< iftest= “birthday != null and birthday != “>
, birthday = #{birthday}
</ if>
</ trim>
where id = ${id}
</ update>
parseDynamicTags 方法的返回值是两个 List,也就是两个 Sql 节点集合。SqlNode 责任编辑一开始已经介绍,分析完解析过程之后会说一下各个 SqlNode 类型的作用。
首先依照 update 节点 (Node) 得到所有的子节点,分别是 3 个子节点:
文档节点 \n UPDATE users trim 子节点 … 文档节点 \n where id = #遍历各个子节点:
假如节点类型是文档或者 CDATA,构造两个 TextSqlNode 或 StaticTextSqlNode; 假如节点类型是原素,说明该 update 节点是个静态 sql,然后会采用 NodeHandler 处理各个类型的子节点。这里的 NodeHandler 是 XMLBuilder 的两个内部接口,其实现类包括 TrimHandler、WhereHandler、SetHandler、IfHandler、ChooseHandler 等。看类名也就明白了这个 Handler 的作用,比如说我们分析的 trim 节点,对应的是 TrimHandler;if 节点,对应的是 IfHandler… 这里子节点 trim 被 TrimHandler 处理,TrimHandler 内部也采用 parseDynamicTags 方法解析节点。遇到子节点是原素的话,重复以上步骤:
trim 子节点内部有 7 个子节点,分别是文档节点、if 节点、是文档节点、if 节点、是文档节点、if 节点、文档节点。文档节点跟以后一样处理,if 节点采用 IfHandler 处理。遍历步骤如上所示,下面我们看下几个 Handler 的实现细节。
IfHandler 处理方法也是采用 parseDynamicTags 方法,然后加上 if 标签必要性的属性:
privateclassIfHandlerimplementsNodeHandler{
publicvoidhandleNode(XNode nodeToHandle, List<SqlNode> targetContents){
List<SqlNode> contents = parseDynamicTags(nodeToHandle);
MixedSqlNode mixedSqlNode = newMixedSqlNode(contents);
String test = nodeToHandle.getStringAttribute( “test”);
IfSqlNode ifSqlNode = newIfSqlNode(mixedSqlNode, test);
targetContents.add(ifSqlNode);
}
}
TrimHandler 处理方法也是采用 parseDynamicTags 方法,然后加上 trim 标签必要性的属性:
privateclassTrimHandler implementsNodeHandler {
publicvoidhandleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
List<SqlNode> contents = parseDynamicTags(nodeToHandle);
MixedSqlNode mixedSqlNode =newMixedSqlNode(contents);
Stringprefix = nodeToHandle.getStringAttribute( “prefix”);
StringprefixOverrides = nodeToHandle.getStringAttribute( “prefixOverrides”);
Stringsuffix = nodeToHandle.getStringAttribute(“suffix”);
StringsuffixOverrides = nodeToHandle.getStringAttribute( “suffixOverrides”);
TrimSqlNode trim = newTrimSqlNode(configuration, mixedSqlNode, prefix, prefixOverrides, suffix, suffixOverrides);
targetContents.add(trim);
}
}
以上 update 方法最终通过 parseDynamicTags 方法得到的 SqlNode 集合如下:
trim 节点:
由于这个 update 方法是个静态节点,因此构造出了 DynamicSqlSource。DynamicSqlSource 内部就能构造 sql 了:
DynamicSqlSource 内部的 SqlNode 属性是两个 MixedSqlNode。然后我们看看各个 SqlNode 实现类的 apply 方法。下面分析一下各个 SqlNode 实现类的 apply 方法实现:
MixedSqlNode:MixedSqlNode 会遍历调用内部各个 sqlNode 的 apply 方法。
publicbooleanapply(DynamicContext context){
for(SqlNode sqlNode : contents) {
sqlNode.apply(context);
}
returntrue;
}
StaticTextSqlNode:直接 append sql 文档。
publicbooleanapply(DynamicContext context){
context.appendSql(text);
returntrue;
}
IfSqlNode:这里的 evaluator 是两个 ExpressionEvaluator 类型的实例,内部采用了 OGNL 处理函数逻辑。
publicbooleanapply(DynamicContext context){
if(evaluator.evaluateBoolean(test, context.getBindings)) {
contents.apply(context);
returntrue;
}
returnfalse;
}
TrimSqlNode:
publicbooleanapply(DynamicContext context){
FilteredDynamicContext filteredDynamicContext =newFilteredDynamicContext(context);
booleanresult = contents.apply(filteredDynamicContext);
filteredDynamicContext.applyAll;
returnresult;
}
publicvoidapplyAll{
sqlBuffer = newStringBuilder(sqlBuffer.toString.trim);
String trimmedUppercaseSql = sqlBuffer.toString.toUpperCase(Locale.ENGLISH);
if(trimmedUppercaseSql.length >0) {
applyPrefix(sqlBuffer, trimmedUppercaseSql);
applySuffix(sqlBuffer, trimmedUppercaseSql);
}
delegate.appendSql(sqlBuffer.toString);
}
privatevoidapplyPrefix(StringBuilder sql, String trimmedUppercaseSql){
if(!prefixApplied) {
prefixApplied = true;
if(prefixesToOverride != null) {
for(String toRemove : prefixesToOverride) {
if(trimmedUppercaseSql.startsWith(toRemove)) {
sql.delete( 0, toRemove.trim.length);
break;
}
}
}
if(prefix != null) {
sql.insert( 0, ” “);
sql.insert( 0, prefix);
}
}
}
TrimSqlNode 的 apply 方法也是调用属性 contents (一般都是 MixedSqlNode) 的 apply 方法,按照实例也就是 7 个 SqlNode,都是 StaticTextSqlNode 和 IfSqlNode。最终会采用 FilteredDynamicContext 过滤掉 prefix 和 suffix。
微软将系统功能与explorer.exe解耦,加速Windows curl译者收下街道社区成员赠送的Mac mini,将用作curl开发
这里有最新开源资讯、软件更新、技术干货等内容
点这里 ↓↓↓ 记得 关注✔ 标星⭐ 哦~