一文掌握 MyBatis 的动态 SQL 使用与原理

2023-02-01 0 789

原副标题:该文掌控 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开发

这里有最新开源资讯、软件更新、技术干货等内容

点这里 ↓↓↓ 记得 关注✔ 标星⭐ 哦~

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务