前言:
而今看官们对“mysql动态sql标签”大致比较看重,大家都想要知道一些“mysql动态sql标签”的相关资讯。那么小编同时在网络上汇集了一些有关“mysql动态sql标签””的相关内容,希望姐妹们能喜欢,兄弟们一起来了解一下吧!想必大家都写过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.参数结构
@Testpublic 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}\n2.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: 22.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 '坏学生' endwhere 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 = nullprefixOverrides = {"AND ","OR ","AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t"}.prefix = WHEREsuffix = null2.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 = SETsuffix = null
标签: #mysql动态sql标签 #java 动态拼接sql