mybatis的动态sql拼接原理
ztj100 2025-01-07 17:23 20 浏览 0 评论
想必大家都写过mysql的动态sql标签(xml标签)吧,常用的有<if>、<choose>、<where>、<foreach>等标签,平时用的时候没有太多的关注mybatis具体的实现.接下来跟着小编的文章,具体的看下这些标签是如何实现sql的动态拼接的吧.
1.概述
当我们写mapper.xml时,当mybatis启动会把我们写的每一个标签转化为一个sqlNode的内存结构,前端程序进行接口调用的时,会把参数通过controller经过service到达我们的mapper.然后我们的sqlNode根据传入的参数,进行动态sql的拼接,再次整合我们的参数结构,然后根据sql语句与二次整合的参数结构进行preparedStatement的set操作,之后执行sql语句.
以下面简单sql为例,我们简单看下sqlNode与mapper参数的数据结构.
-- sql语句
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`role_id` int(11) DEFAULT NULL COMMENT '角色id',
`name` varchar(40) DEFAULT NULL COMMENT '用户名称',
`alisa` varchar(40) DEFAULT NULL COMMENT '别名',
`tag` tinyint(4) DEFAULT NULL COMMENT '1、使用name,2、使用alisa',
`sex` tinyint(3) DEFAULT NULL COMMENT '性别:1、男,2、女',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
<!--mapper.xml语句-->
<select id="getUserListByIf" resultMap="userEntity">
select id,name,alisa,tag from user
<if test="id != null">
where id = #{id}
</if>
</select>
1.1.sqlNode内存结构
让我们看下形成的sqlNode与参数的内存结构吧:
从上图我们就能看到以下几条信息
- 整个select语句就是一个大的MixedSqlNode.
- 所有的标签node(if,where)等都有自己的sqlNode实现,而且其内部也会指向也是一个MixSqlNode.
- 所有的叶子节点都是StaticTextSqlNode,也就是我们需要动态拼接的sql.
1.2.参数结构
@Test
public void getUserListByIf() {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.getUserListByIf(1);
}
User getUserListByIf(@Param("id") Integer id);
从上图可以看出,我们传的参数1,会被存放到一个Map中,会作为键值对的形式进行缓存,那么为什么还有一个param1参数呢?这个我们下期再讲.
1.3.小结
综上不难看出,sqlNode其实是一个树状结构,当我们进行动态sql拼接时,需要根据一定的条件,将叶子节点的StaticTextSqlNode中的text字符串拼接起来,形成一个临时的sql字符串(将临时的#{xx}替换成{?}才是最终的sql).而前端传的参数是一个map结构.
2.常用sqlNode
mybatis中常用的sql标签有以下几种:
<if>、<bind>、<foreach>、<where>、<set>、<choose><when><otherwise></choose>、<trim>.我们来看一下这些sqlNode的继承关系图.
结构我们看到了,接下来我们一一来剖析各个标签的细节.
2.1.if
参见sqlNode继承图我们知道<if>标签对应的是ifSqlNode,那我们看下ifSqlNode的代码吧
public class IfSqlNode implements SqlNode {
private final ExpressionEvaluator evaluator;
// 如第一节所述,这个test就是我们if标签中的test内容,即:id != null
private final String test;
// 这个就是ifSqlNode下挂的子节点,如第一节内存结构图,这是一个MixSqlNode,
// 其中有一个List成员变量,
// 用于存放其他类型的SqlNode,这也说明<if>标签内可以嵌套其他类型的标签
private final SqlNode contents;
public IfSqlNode(SqlNode contents, String test) {
this.test = test;
this.contents = contents;
this.evaluator = new ExpressionEvaluator();
}
@Override
public boolean apply(DynamicContext context) {
// 这里是通过ognl表达式判断我们写的test的条件是否满足
// 其中test = id != null
// context.getBindings,返回的是一个Map,其中有{id:1,param1:1}
if (evaluator.evaluateBoolean(test, context.getBindings())) {
// 如果满足了,执行子标签的sql拼接动作.
contents.apply(context);
return true;
}
return false;
}
}
MixSqlNode是一个核心的结构,几乎大部分sql的拼接都少不了他的存在,他起到一个承上启下的作用.
public class MixedSqlNode implements SqlNode {
// 包含的下层的sqlNode集合
private final List<SqlNode> contents;
public MixedSqlNode(List<SqlNode> contents) {
this.contents = contents;
}
@Override
public boolean apply(DynamicContext context) {
// 从这里不能看出,是遍历MixSqlNode下的所有子节点,进行一个字符串的拼接操作,
// 当然各个sqlNode有各自不同的apply方法,也就有着不同的sql拼接动作.
for (SqlNode node : contents) {
node.apply(context);
}
return true;
}
}
StaticTextSqlNode是一个最终形成的需要拼接的sql片段,大部分的sql拼接都是由该sqlNode完成的.
public class StaticTextSqlNode implements SqlNode {
// 如第一节中例子中的IfSqlNode的话,那么这里就是 where id = #{id}
private final String text;
public StaticTextSqlNode(String text) {
this.text = text;
}
@Override
public boolean apply(DynamicContext context) {
// 看这里就是进行的sql拼接,这里context.appendSql
// 就是调用了context中的StringJoiner
// 进行的一个append操作
context.appendSql(text);
return true;
}
}
由第一节的内存结构图可以得知整个sql拼接的过程,这里在放一下内存结构图.
好了,我们来看下<if>标签的拼接流程
- 执行rootSqlNode的apply动作,此时1号节点MixedSqlNode的apply动作为遍历contents,分别执行相应sqlNode的apply操作.
- 发现第一个元素为2号节点,类型为StaticTextSqlNode,然后执行3号节点StaticTextSqlNode的apply操作,这时总的sql语句为select id,name,alisa,tag from user.
- 发现第二个元素为3号节点IfSqlNode,然后执行IfSqlNode的apply操作,根据ognl表达式判断id = 1不为null满足,所以执行3号节点IfSqlNode中的成员变量contents的apply操作,此时contents为MixSqlNode
- 执行4号节点MixSqlNode的apply操作时,遍历其中的contents集合,发起其中只有一个5号节点StaticTextSqlNode,随即进行sql的拼接,此时拼接好的sql语句为select id,name,alisa,tag from user where id = #{id}
- 5号节点StaticTextSqlNode拼接完成后出栈,回到上层4号节点MixedSqlNode再出栈,回到3号节点IfSqlNode,再出栈,回到1号节点rootSqlNode,此时发现其contents中还有一个6号节点StaticTextSqlNode需要执行apply方法,随即在sql后面将\n拼接到了后面,此时得到最终的sql语句为select id,name,alisa,tag from user where id = #{id}\n
2.2.bind
bind标签与其他标签不一样,因为该标签不涉及sql的拼接,该标签的作用就是将参数经过该标签规则的转换,形成另外的参数,然后在放回到原参数集合中,其实这个动作也可以完全在我们的service层处理掉.
public class VarDeclSqlNode implements SqlNode {
// bind标签上写的name
private final String name;
// bind标签上写的value,可见bind标签上的value可以写表达式
private final String expression;
public VarDeclSqlNode(String var, String exp) {
// bind标签上写的name
name = var;
// bind标签上写的value
expression = exp;
}
@Override
public boolean apply(DynamicContext context) {
// 这里可以看出bind标签的value可以写ognl表达式,而且不用写#{}
// 获取接口传的参数,因为context对应的参数,都是存放在map中的,
// 这里不需要进行#{}动态替换,参见parameterMapping
final Object value = OgnlCache.getValue(expression, context.getBindings());
// 当调用bind标签对应的SqlNode的拼接方法(apply)时,
// 这里会将接口的参数进行expression转换 并将结果绑定
// 到bind标签的name上,并放回到接口的参数结构中,
// 形成了新的参数名和处理后的参数值在后续#{}替换时,
// 该新的参数名和参数值就可以直接使用了.
context.bind(name, value);
return true;
}
}
<select id="getByName" resultMap="userEntity">
<!-- 注意,这里的value是ongl表达式,这里还支持自定义参数处理的方法,
比如使用工具类的静态方法@Utils@Method(),详情参考ognl表达式 -->
<bind name="name" value="'%' + name + '%'"/>
select * from user where name like #{name}
</select>
public void getByName() {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<User> users = userMapper.getByName("名字");
System.out.println(123);
}
List<User> getByName(String name);
DEBUG [main] - ==> Preparing: select * from user where name like ?
DEBUG [main] - ==> Parameters: %名字%(String)
DEBUG [main] - <== Total: 2
2.3.foreach
该标签的sql拼接与参数的多少有很大关系,首先我们看下<foreach>标签的基本结构.
<foreach collection="list" separator="," item="user" index="i" open= "(" close = ")">
</foreach>
public class ForEachSqlNode implements SqlNode {
// 临时替换的item的前缀字符
public static final String ITEM_PREFIX = "__frch_";
private final ExpressionEvaluator evaluator;
// 该集合放到传入参数中的key
private final String collectionExpression;
// 这个contents是改sql节点下包含的子节点,
// 如这是个foreach节点,下面可能包含if节点等.
// 如果是MixedSqlNode,则需要继续解析,
// 如果是StaticSqlNode只需要将对应的文字信息拼接上即可
private final SqlNode contents;
// foreach标签的open属性,在标签内容的开始添加,
// 只加一次,不是循环几次加几次
private final String open;
// foreach标签的close属性,在标签内容的结束添加,
// 只加一次,不是循环几次加几次
private final String close;
// foreach标签的separator属性,循环几次加几次,
// 每次都加在该行的头部,第一次循环不加
private final String separator;
// foreach的item,这个字段随便定义,
// 这个会与上面的__frch_进行组装,形成每一个item的名字
private final String item;
// foreach的index,这个字段也随便定义,
// 该字段与__frch_item_index会组成定位每一个item的map的key
private final String index;
private final Configuration configuration;
}
接下来我们看看ForeachSqlNode的apply方法
public boolean apply(DynamicContext context) {
Map<String, Object> bindings = context.getBindings();
final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
// 可以是Set,ArrayList,如果list长度为0,那么这里返回
if (!iterable.iterator().hasNext()) {
return true;
}
boolean first = true;
// 这里是添加我们的写的open属性,比如常用的"(",
// 从这也能看出,open属性只是在开始的时候拼接,而不在每次
// 循环的时候拼接
applyOpen(context);
int i = 0;
// 这里是List集合中的内容,集合中有几个,就会执行几次
for (Object o : iterable) {
DynamicContext oldContext = context;
// 每一个List中的项都被封装为一个DynamicContext
// 如果是第一个元素或者没有separator属性,会在sql组装时添加"",相当于什么都没加
// 第二个及以后的元素需要添加我们写的分隔符,比如:,
if (first || separator == null) {
// 这里的PrefixedContext为DynamicContext的包装类,
// 每次都要为context创建一个包装类,进行sql拼接
// 当每一行的sql处理完的时候,会将context指回原context.
context = new PrefixedContext(context, "");
} else {
context = new PrefixedContext(context, separator);
}
// 这里的context虽然是包装过的PrefixedContext,
// 但是getUniqueNumber仍然调用的是原DynamicContext的
// 生成唯一id的方法,其实这个就是为了生成我们集合数组的下标.0,1,2,3...
int uniqueNumber = context.getUniqueNumber();
// Issue #709
// 如果是Entry
if (o instanceof Map.Entry) {
// 这里如果是Map.Entry结构
Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
applyIndex(context, mapEntry.getKey(), uniqueNumber);
applyItem(context, mapEntry.getValue(), uniqueNumber);
} else {
applyIndex(context, i, uniqueNumber);
// 这里将参数map中生成对应的key,value.如:__frch_user_0 --> user(对象)
applyItem(context, o, uniqueNumber);
}
// 这里是进行sql语句拼接
// 将foreach标签中写的#{user.roleId},替换成#{__frch_user_0.roleId}
contents.apply(new FilteredDynamicContext(
configuration, context, index, item, uniqueNumber));
if (first) {
first = !((PrefixedContext) context).isPrefixApplied();
}
// 这里又将context指针指向原来的DynamicContext
context = oldContext;
i++;
}
// 这里是添加我们的写的close属性,比如常用的")",
// 从这也能看出,close属性只是在结束的时候拼接,而不在每次
// 循环的时候拼接
applyClose(context);
// 这里将bind中的临时存的item删除,就是我们foreach中定义的item,对sql拼接没用
context.getBindings().remove(item);
// 这里将bind中的临死存的index删除,就是我们foreach中定义的index,对sql拼接没用
context.getBindings().remove(index);
// 其实这里对参数map存了两份索引,一份是item索引如【__frch_user_0】,
// 另一份index索引如【__frch_i_0】
return true;
}
接下来我们看下<foreach>标签在内存中的结构
public void batchInsertUser() {
ArrayList<User> users = new ArrayList<>();
User user1 = new User();
users.add(user1);
User user2 = new User();
users.add(user2);
userMapper.batchInsert(users);
}
void batchInsert(List<User> users);
注:我们这里想要拼成的sql为:
insert into user (role_id,name,alisa,tag) values (?,?,?,?),(?,?,?,?)
<insert id="batchInsert">
insert into user (role_id,name,alisa,tag) values
// 这里如果mapper传的参数只有一个且为Collection类型,
// 那么参数map中的key为list或者collection
// 为什么这里没写open,close呢?因为这个属性只在循环的外面加(只加一次),
// 而不是每次循环结束后
<foreach collection="list" separator="," item="user" index="i" >
(#{user.roleId},#{user.name},#{user.alisa},#{user.tag})
</foreach>
</insert>
接下来我们看下内存结构图吧
我们总结下<foreach>sql拼接的流程
- 执行rootSqlNode的apply动作,此时1号节点MixedSqlNode的apply动作为遍历contents,分别执行相应sqlNode的apply操作
- 发现第一个元素为2号节点,类型为StaticTextSqlNode,然后执行3号节点StaticTextSqlNode的apply操作,这时总的sql语句为insert into user (role_id,name,alisa,tag) values.
- 发现第二个元素为3号节点ForEachSqlNode,然后执行ForEachSqlNode的apply操作,发现没有设置open属性所以不设置open
- 然后遍历我们的users,发现其中有两个user对象.然后循环遍历这两个user,此时根据item前缀__frch_,以及item属性user,以及我们集合的下标(从0开始),生成唯一的对象key:__frch_user_0,对应的value就是我们list中的0号user.
- 然后执行sql拼接操作,进入到4号节点,遍历4号节点下的5号节点StaticTextSqlNode,然后进行sql替换,即(#{user.roleId},#{user.name},#{user.alisa},#{user.tag}),替换成(#{__frch_user_0.roleId},#{__frch_user_0.name},#{__frch_user_0.alisa},#{__frch_user_0.tag}),然后进行sql拼接,此时sql为insert into user (role_id,name,alisa,tag) values (#{__frch_user_0.roleId},#{__frch_user_0.name},#{__frch_user_0.alisa},#{__frch_user_0.tag}).,执行完之后返回到4号节点,再返回到3号节点.
- 然后再循环遍历第二个元素,生成对应的sql为,(#{__frch_user_1.roleId},#{__frch_user_1.name},#{__frch_user_01.alisa},#{__frch_user_1.tag}).然后在进行sql拼接insert into user (role_id,name,alisa,tag) values (#{__frch_user_0.roleId},#{__frch_user_0.name},#{__frch_user_0.alisa},#{__frch_user_0.tag}),(#{__frch_user_1.roleId},#{__frch_user_1.name},#{__frch_user_01.alisa},#{__frch_user_1.tag}).
整个sql拼接完成了,在第3步的时候,生成了相应的参数映射__frch_user_0 --> user[0].后面就是参数替换的问题了,下节我们在分享.上面这个例子是insert的batch操作,我们平时还有一种场景,就是使用in查询,需要将相应的id拼接到括号后面.
<select id="selectByIds">
select * from user where id in
<foreach collection="list" separator="," item="user" index="i" open = "(" close = ")">
#{user.id}
</foreach>
</select>
// 首先我们看下我们最终想要的sql是:select * from user where id in (1,2,3,4).
// 那么这里为什么要加open和close属性呢?我们只想在
// user列表前面加上"("和user列表结束之后加上")",
// 而不是在除第一次之外每次循环前都加上separator,
2.4.choose..when..otherwise
前面我们已经讲过<choose>标签对应的SqlNode为ChooseSqlNode,<when>标签对应的是IfSqlNode,而<otherwise>没有自己单独的标签,对应的MixSqlNode.那我们看下ChooseSqlNode的结构吧
public class ChooseSqlNode implements SqlNode {
// 这是otherwise标签下的sqlNode,这个SqlNode的类型为MixSqlNode
private final SqlNode defaultSqlNode;
// 这是when标签下的所有sqlNode,这个列表中SqlNode的类型为IfSqlNode
private final List<SqlNode> ifSqlNodes;
public ChooseSqlNode(List<SqlNode> ifSqlNodes, SqlNode defaultSqlNode) {
this.ifSqlNodes = ifSqlNodes;
this.defaultSqlNode = defaultSqlNode;
}
@Override
public boolean apply(DynamicContext context) {
// 看到了吧,这里就是when标签对应的sqlNode,
// 只要有一个when满足的话,这里就会reture了
for (SqlNode sqlNode : ifSqlNodes) {
// 这里是IfSqlNode的拼接,可以参见2.1.
if (sqlNode.apply(context)) {
return true;
}
}
// 如果遍历完所有的when还没有找到对应的sqlNode,那么这里就会走otherwise了.
// 看看是不是跟我们的java代码switch..case..default很像啊.
if (defaultSqlNode != null) {
// 这里是MixSqlNode的拼接,会遍历MixSqlNode下的所有SqlNode,
// 分别进行相应的apply操作
defaultSqlNode.apply(context);
return true;
}
return false;
}
}
那么我们来举例说明下吧
public void getUserListByChoose() {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<User> userList = userMapper.getUserListByChoose("male");
}
List<User> getUserListByChoose(@Param("sex") String sex);
<select id="getUserListByChoose" resultMap="userEntity">
select id,name,alisa,tag from user where
<choose>
<when test="sex == 'male'">
sex = 1
</when>
<when test="sex == 'female'">
sex = 3
</when>
<otherwise>
sex = 2
</otherwise>
</choose>
</select>
我们看下ChooseSqlNode的内存结构
由于choose标签基本使用的是if标签的内容,这里就不做果断阐述,看下sql拼接的流程:
- 执行rootSqlNode的apply动作,此时1号节点MixedSqlNode的apply动作为遍历contents,分别执行相应sqlNode的apply操作
- 然后进入2号节点,发现是个StaticTextSqlNode,这时会把该sql直接拼接上,此时sql为select id,name,alisa,tag from user where.
- 然后进入3号节点发现是个ChooseSqlNode,这里会优先遍历ChooseSqlNode中的IfSqlNode,也就是<when>标签.
- 此时进入第一个<when>标签对应的IfSqlNode,也就是进入到6号节点,判断sex == 'male',发现是true.此时会进入到该IfSqlNode的MixSqlNode的apply操作
- 所以此时进入到7号节点,然后进入到8号节点.发现是StaticTextSqlNode.将字符串拼接到sql上,此时的sql为select id,name,alisa,tag from user where sex = 1.
- 此时依次将8、7、6出栈,回到3节点,3节点就是我们的ChooseSqlNode的apply操作.遍历IfSqlNode时成功了会return,此时3号节点也出栈
- 然后回到12节点将\n拼接到sql上,此时sql为select id,name,alisa,tag from user where sex = 1.
2.5.trim
首先我们看下<trim>标签的结构
public class TrimSqlNode implements SqlNode {
// 这里的contents为MixedSqlNode
private final SqlNode contents;
// 需要在该标签组成的语句最前面添加的字符,比如where,set
private final String prefix;
// 需要在该标签组成的语句最前面添加的字符
private final String suffix;
// 如果该标签下的第一个字符在这个list里面,需要去掉,注意只能剔除一次
private final List<String> prefixesToOverride;
// 如果该标签下的最后一个字符在这个list里面,需要去掉,注意只能剔除一次
private final List<String> suffixesToOverride;
private final Configuration configuration;
public TrimSqlNode(Configuration configuration, SqlNode contents,String prefix,
String prefixesToOverride, String suffix, String suffixesToOverride) {
this(configuration, contents, prefix, parseOverrides(prefixesToOverride),
suffix, parseOverrides(suffixesToOverride));
}
protected TrimSqlNode(Configuration configuration, SqlNode contents,
String prefix, List<String> prefixesToOverride,
String suffix, List<String> suffixesToOverride) {
// 这里是解析xml文件时生成的,不是执行sql语句是生成的
this.contents = contents;
this.prefix = prefix;
this.prefixesToOverride = prefixesToOverride;
this.suffix = suffix;
this.suffixesToOverride = suffixesToOverride;
this.configuration = configuration;
}
@Override
public boolean apply(DynamicContext context) {
FilteredDynamicContext filteredDynamicContext
= new FilteredDynamicContext(context);
// 这里是调用MixedSqlNode的apply方法,
// 就是遍历MixedSqlNode中的List<SqlNode>,
// 并调用SqlNode的apply方法
// 这里应该是将and添加进来了
boolean result = contents.apply(filteredDynamicContext);
filteredDynamicContext.applyAll();
return result;
}
// 这里是处理的override属性的
private static List<String> parseOverrides(String overrides) {
if (overrides != null) {
// 这里明显能看出,prefixesToOverride、suffixesToOverride
// 可以写多个想去除前后缀的方法
// 多个的话需要用"|"拼接
final StringTokenizer parser = new StringTokenizer(overrides, "|", false);
final List<String> list = new ArrayList<>(parser.countTokens());
while (parser.hasMoreTokens()) {
list.add(parser.nextToken().toUpperCase(Locale.ENGLISH));
}
return list;
}
return Collections.emptyList();
}
private class FilteredDynamicContext extends DynamicContext {
// 处理sql
public void applyAll() {
sqlBuffer = new StringBuilder(sqlBuffer.toString().trim());
String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH);
if (trimmedUppercaseSql.length() > 0) {
// 处理头
applyPrefix(sqlBuffer, trimmedUppercaseSql);
// 处理尾
applySuffix(sqlBuffer, trimmedUppercaseSql);
}
// 然后将处理完的sql拼接上
delegate.appendSql(sqlBuffer.toString());
}
// 处理前缀
private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {
// 处理头
// 这是where条件的第一次进来,第一个where跟的条件是不允许加and,or等前缀的
if (!prefixApplied) {
// 当第一次进来之后,就会把这个prefixApplied设为true,
// 保证where、set标签有多个and或者or的时候,不会全部剔除.只剔除第一个
prefixApplied = true;
if (prefixesToOverride != null) {
//
for (String toRemove : prefixesToOverride) {
// 这里是剔除sql片段中的前缀,从break也能看出如果有一个命中的话就会break.
if (trimmedUppercaseSql.startsWith(toRemove)) {
sql.delete(0, toRemove.trim().length());
break;
}
}
}
// 这边也是第一次进来,如果没有空格会添加上空格,
// 保证sql语句的正确性,这也是保证where、set标签
// 有多个and或者or的时候只有第一次进来时才会添加where
if (prefix != null) {
sql.insert(0, " ");
// 这里是加入where
sql.insert(0, prefix);
}
}
}
// 处理后缀
private void applySuffix(StringBuilder sql, String trimmedUppercaseSql) {
// 处理尾
if (!suffixApplied) {
// 第二次进来就不会处理尾了
suffixApplied = true;
if (suffixesToOverride != null) {
// 这里也是,如果sql的尾部是以我们配置的suffixesToOverride结尾,会剔除
// 也是只能剔除一次
for (String toRemove : suffixesToOverride) {
if (trimmedUppercaseSql.endsWith(toRemove)
|| trimmedUppercaseSql.endsWith(toRemove.trim())) {
int start = sql.length() - toRemove.trim().length();
int end = sql.length();
sql.delete(start, end);
break;
}
}
}
// 这里也是添加空格和尾部
if (suffix != null) {
sql.append(" ");
sql.append(suffix);
}
}
}
}
}
平常我们用<trim>标签用少,基本用的都是<where>、<set>标签,这两个标签是简易的<trim>标签,从继承图上我们也能看出TrimSqlNode是WhereSqlNode和SetSqlNode的父类.所有WhereSqlNode和SetSqlNode中的方法都是调用的TrimSqlNode中的方法.平时只有批量更新的时候需要使用<trim>标签.首先我们要看下批量更新的sql该怎么写.一般根据id去批量更新某一个相同值的话是比较简单的.比如:想要更某些学生的标签.相应的sql为update user set alisa = '好学生' where id in (1,2,3,4).这个用<foreach>标签就能实现.
<update id = "updateByIds">
update user set alisa = '好学生' where id in
<foreach collection="list" separator="," item="user" index="i" open= "(" close = ")">
#{user.id}
</foreach>
</update>
但是如果我想更新根据不同的学生去更新不同的tag怎么办呢?就是当id=1时tag='好学生',id=2时tag='坏学生',这种sql语句就不太够用了.那么一般会用到sql的case when语句来处理这种情况.想要拼成的最终sql语句为
update user set
alisa = case id
when 1 then '好学生'
when 2 then '坏学生' end
where id in (1,2);
这种sql语句用之前的标签都无法完成,只有使用<trim>标签才可以
public void batchUpdate() {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
ArrayList<User> users = new ArrayList<>();
User user1 = new User();
users.add(user1);
user1.setId(1);
user1.setAlisa("别名1");
User user2 = new User();
users.add(user2);
user2.setId(2);
user2.setAlisa("别名2");
userMapper.batchUpdate(users);
sqlSession.commit();
}
<update id="batchUpdate">
update user set
<!-- 如果该标签的最后字符包含","需要剔除 -->
<trim suffixOverrides=",">
<!-- 需要添加的前缀 -->
<trim prefix="alisa = ">
<!-- 拼接case when 语句 -->
<foreach collection="list" open="case " close="end,"
separator="" index="i" item="user">
when id = #{user.id} then #{user.alisa}
</foreach>
</trim>
</trim>
where id in
<!-- 拼接in语句 -->
<foreach collection="list" open="(" close=")"
separator="," index="i" item="user">
#{user.id}
</foreach>
</update>
这个xml的内存结构比较复杂,嵌套的参数比较多,图太大无法放下,大家可以自行尝试画一下,具体的内存结构可以在DynamicSqlSource类中看到,具体位置参见如下代码
public class DynamicSqlSource implements SqlSource {
private final Configuration configuration;
// 这里的rootSqlNode就是我们sqlNode的根节点
private final SqlNode rootSqlNode;
public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
this.configuration = configuration;
this.rootSqlNode = rootSqlNode;
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
DynamicContext context = new DynamicContext(configuration, parameterObject);
// debug到这里能看到rootSqlNode的结构
rootSqlNode.apply(context);
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ?
Object.class : parameterObject.getClass();
// 如果是PreparedStatementHandler的话,这里将特殊符号转换成?
// 2.将初步解析的动态sqlNode,进行"?"替换,并定位不同"?"对应的不同参数解析
SqlSource sqlSource = sqlSourceParser.parse(
context.getSql(), parameterType, context.getBindings());
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
// 这是把context中的参数定位的map放入boundSql中
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}
}
这里总结出几个重点
<trim suffixOverrides=",|." prefixOverrides="and|or" prefix="where" suffix=",">
</trim>
- suffixOverrides作用是在trim标签内拼接的sql会把以包含suffixOverrides结尾的字符剔除,只剔除一次.比如:该trim片段拼接到最后为set name = '张三',这时会把最后一个【,】剔除得到最终sql为set name = '张三'.这个一般用在<set>标签上.保证最后一个set结尾不带【,】
- prefixOverrides作用是在trim标签内拼接的sql会把以包含prefixOverrides开头的字符剔除,只剔除一次.比如:该trim片段拼接到最后为and name = '张三',这时会把第一个【and|or】剔除得到最终sql为name = '张三'.这个一般用在<where>标签上.保证第一个where条件不带and或者or.
- prefix作用是在该标签的头部添加prefix内容,一般也是用在where和set语句上,保证where字符的添加
- suffix作用是在该标签的尾部添加suffix内容.目前暂时没有任何标签用到它,可自行使用.
- 这里还有一点,这里执行的顺序是2、3、1、4.先剔除头部、在添加头部、在剔除尾部、在添加尾部.可以从源码TrimSqlNode.FilteredDynamicContext的apply方法可以看到.
2.6.where
我们看下<where>标签的结构
public class WhereSqlNode extends TrimSqlNode {
// 从这可以看出WhereSqlNode其实就是在TrimSqlNode初始化了prefixOverrides,即
// 需要剔除的首部字符,以及需要添加的首部字符WHERE
private static List<String> prefixList =
Arrays.asList("AND ","OR ","AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t");
public WhereSqlNode(Configuration configuration, SqlNode contents) {
super(configuration, contents, "WHERE", prefixList, null, null);
}
}
其实这个完全根据TrimSqlNode来处理,然后我们举个例子看下吧
public void getUserListByWhere() {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<User> userList = userMapper.getUserListByWhere(1);
}
List<User> getUserListByWhere(@Param("id") Integer id);
<select id="getUserListByWhere" resultMap="userEntity">
select id,name,alisa,tag from user
<where>
and id = #{id}
</where>
</select>
下面我们看下TrimSqlNode对应的内存数据结构
执行顺序大家可以参照if自行补充一下.这里只补充一下:其实<where>标签也是基于<trim>标签,只不过四个参数都不能修改
- suffixOverrides = null
- prefixOverrides = {"AND ","OR ","AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t"}.
- prefix = WHERE
- suffix = null
2.7.set
我们看下<set>标签的结构
public class SetSqlNode extends TrimSqlNode {
private static final List<String> COMMA = Collections.singletonList(",");
public SetSqlNode(Configuration configuration,SqlNode contents) {
super(configuration, contents, "SET", COMMA, null, COMMA);
}
}
其实这个完全根据TrimSqlNode来处理,是不是发现跟WhereSqlNode很像啊,对的.这个就不做介绍了,他也是不支持这4个参数的修改,这4个参数的内容如下:
- suffixOverrides = {","}
- prefixOverrides = {"AND ","OR ","AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t"}.
- prefix = SET
- suffix = null
- 上一篇:MyBatis的10种用法
- 下一篇:一个时间戳精度问题,引发了一个MySQL血案
相关推荐
- Whoosh,纯python编写轻量级搜索工具
-
引言在许多应用程序中,搜索功能是至关重要的。Whoosh是一个纯Python编写的轻量级搜索引擎库,可以帮助我们快速构建搜索功能。无论是在网站、博客还是本地应用程序中,Whoosh都能提供高效的全文搜...
- 如何用Python实现二分搜索算法(python二分法查找代码)
-
如何用Python实现二分搜索算法二分搜索(BinarySearch)是一种高效的查找算法,适用于在有序数组中快速定位目标值。其核心思想是通过不断缩小搜索范围,每次将问题规模减半,时间复杂度为(O...
- 路径扫描 -- dirsearch(路径查找器怎么使用)
-
外表干净是尊重别人,内心干净是尊重自己,干净,在今天这个时代,应该是一种极高的赞美和珍贵。。。----网易云热评一、软件介绍Dirsearch是一种命令行工具,可以强制获取web服务器中的目录和文件...
- 78行Python代码帮你复现微信撤回消息!
-
来源:悟空智能科技本文约700字,建议阅读5分钟。本文基于python的微信开源库itchat,教你如何收集私聊撤回的信息。...
- 从零开始学习 Python!2《进阶知识》 Python进阶之路
-
欢迎来到Python学习的进阶篇章!如果你说已经掌握了基础语法,那么这篇就是你开启高手之路的大门。我们将一起探讨面向对象编程...
- 白帽黑客如何通过dirsearch脚本工具扫描和收集网站敏感文件
-
一、背景介绍...
- Python之txt数据预定替换word预定义定位标记生成word报告(四)
-
续接Python之txt数据预定替换word预定义定位标记生成word报告(一)https://mp.toutiao.com/profile_v4/graphic/preview?pgc_id=748...
- Python——字符串和正则表达式中的反斜杠('\')问题详解
-
在本篇文章里小编给大家整理的是关于Python字符串和正则表达式中的反斜杠('\')问题以及相关知识点,有需要的朋友们可以学习下。在Python普通字符串中在Python中,我们用'\'来转义某些普通...
- Python re模块:正则表达式综合指南
-
Python...
- python之re模块(python re模块sub)
-
re模块一.re模块的介绍1.什么是正则表达式"定义:正则表达式是一种对字符和特殊字符操作的一种逻辑公式,从特定的字符中,用正则表达字符来过滤的逻辑。(也是一种文本模式;)2、正则表达式可以帮助我们...
- MySQL、PostgreSQL、SQL Server 数据库导入导出实操全解
-
在数字化时代,数据是关键资产,数据库的导入导出操作则是连接数据与应用场景的桥梁。以下是常见数据库导入导出的实用方法及代码,包含更多细节和特殊情况处理,助你应对各种实际场景。一、MySQL数据库...
- Zabbix监控系统系列之六:监控 mysql
-
zabbix监控mysql1、监控规划在创建监控项之前要尽量考虑清楚要监控什么,怎么监控,监控数据如何存储,监控数据如何展现,如何处理报警等。要进行监控的系统规划需要对Zabbix很了解,这里只是...
- mysql系列之一文详解Navicat工具的使用(二)
-
本章内容是系列内容的第二部分,主要介绍Navicat工具的使用。若查看第一部分请见:...
你 发表评论:
欢迎- 一周热门
- 最近发表
-
- Whoosh,纯python编写轻量级搜索工具
- 如何用Python实现二分搜索算法(python二分法查找代码)
- 路径扫描 -- dirsearch(路径查找器怎么使用)
- 78行Python代码帮你复现微信撤回消息!
- 从零开始学习 Python!2《进阶知识》 Python进阶之路
- 白帽黑客如何通过dirsearch脚本工具扫描和收集网站敏感文件
- Python之txt数据预定替换word预定义定位标记生成word报告(四)
- 假期苦短,我用Python!这有个自动回复拜年信息的小程序
- Python——字符串和正则表达式中的反斜杠('\')问题详解
- Python re模块:正则表达式综合指南
- 标签列表
-
- idea eval reset (50)
- vue dispatch (70)
- update canceled (42)
- order by asc (53)
- spring gateway (67)
- 简单代码编程 贪吃蛇 (40)
- transforms.resize (33)
- redisson trylock (35)
- 卸载node (35)
- np.reshape (33)
- torch.arange (34)
- node卸载 (33)
- npm 源 (35)
- vue3 deep (35)
- win10 ssh (35)
- exceptionininitializererror (33)
- vue foreach (34)
- idea设置编码为utf8 (35)
- vue 数组添加元素 (34)
- std find (34)
- tablefield注解用途 (35)
- python str转json (34)
- java websocket客户端 (34)
- tensor.view (34)
- java jackson (34)