龙空技术网

mybatis的动态sql拼接原理

上进的小镇青年 1624

前言:

而今看官们对“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与参数的内存结构吧:

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的继承关系图.

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拼接的过程,这里在放一下内存结构图.

IfSqlNode

好了,我们来看下<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: 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>

接下来我们看下内存结构图吧

ForeachSqlNode内存结构图

我们总结下<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的内存结构

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对应的内存数据结构

WhereSqlNode内存结构

执行顺序大家可以参照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