百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分类 > 正文

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与参数的内存结构吧:

从上图我们就能看到以下几条信息

  1. 整个select语句就是一个大的MixedSqlNode.
  2. 所有的标签node(if,where)等都有自己的sqlNode实现,而且其内部也会指向也是一个MixSqlNode.
  3. 所有的叶子节点都是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>标签的拼接流程

  1. 执行rootSqlNode的apply动作,此时1号节点MixedSqlNode的apply动作为遍历contents,分别执行相应sqlNode的apply操作.
  2. 发现第一个元素为2号节点,类型为StaticTextSqlNode,然后执行3号节点StaticTextSqlNode的apply操作,这时总的sql语句为select id,name,alisa,tag from user.
  3. 发现第二个元素为3号节点IfSqlNode,然后执行IfSqlNode的apply操作,根据ognl表达式判断id = 1不为null满足,所以执行3号节点IfSqlNode中的成员变量contents的apply操作,此时contents为MixSqlNode
  4. 执行4号节点MixSqlNode的apply操作时,遍历其中的contents集合,发起其中只有一个5号节点StaticTextSqlNode,随即进行sql的拼接,此时拼接好的sql语句为select id,name,alisa,tag from user where id = #{id}
  5. 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拼接的流程

  1. 执行rootSqlNode的apply动作,此时1号节点MixedSqlNode的apply动作为遍历contents,分别执行相应sqlNode的apply操作
  2. 发现第一个元素为2号节点,类型为StaticTextSqlNode,然后执行3号节点StaticTextSqlNode的apply操作,这时总的sql语句为insert into user (role_id,name,alisa,tag) values.
  3. 发现第二个元素为3号节点ForEachSqlNode,然后执行ForEachSqlNode的apply操作,发现没有设置open属性所以不设置open
  4. 然后遍历我们的users,发现其中有两个user对象.然后循环遍历这两个user,此时根据item前缀__frch_,以及item属性user,以及我们集合的下标(从0开始),生成唯一的对象key:__frch_user_0,对应的value就是我们list中的0号user.
  5. 然后执行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号节点.
  6. 然后再循环遍历第二个元素,生成对应的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拼接的流程:

  1. 执行rootSqlNode的apply动作,此时1号节点MixedSqlNode的apply动作为遍历contents,分别执行相应sqlNode的apply操作
  2. 然后进入2号节点,发现是个StaticTextSqlNode,这时会把该sql直接拼接上,此时sql为select id,name,alisa,tag from user where.
  3. 然后进入3号节点发现是个ChooseSqlNode,这里会优先遍历ChooseSqlNode中的IfSqlNode,也就是<when>标签.
  4. 此时进入第一个<when>标签对应的IfSqlNode,也就是进入到6号节点,判断sex == 'male',发现是true.此时会进入到该IfSqlNode的MixSqlNode的apply操作
  5. 所以此时进入到7号节点,然后进入到8号节点.发现是StaticTextSqlNode.将字符串拼接到sql上,此时的sql为select id,name,alisa,tag from user where sex = 1.
  6. 此时依次将8、7、6出栈,回到3节点,3节点就是我们的ChooseSqlNode的apply操作.遍历IfSqlNode时成功了会return,此时3号节点也出栈
  7. 然后回到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>
  1. suffixOverrides作用是在trim标签内拼接的sql会把以包含suffixOverrides结尾的字符剔除,只剔除一次.比如:该trim片段拼接到最后为set name = '张三',这时会把最后一个【,】剔除得到最终sql为set name = '张三'.这个一般用在<set>标签上.保证最后一个set结尾不带【,】
  2. prefixOverrides作用是在trim标签内拼接的sql会把以包含prefixOverrides开头的字符剔除,只剔除一次.比如:该trim片段拼接到最后为and name = '张三',这时会把第一个【and|or】剔除得到最终sql为name = '张三'.这个一般用在<where>标签上.保证第一个where条件不带and或者or.
  3. prefix作用是在该标签的头部添加prefix内容,一般也是用在where和set语句上,保证where字符的添加
  4. suffix作用是在该标签的尾部添加suffix内容.目前暂时没有任何标签用到它,可自行使用.
  5. 这里还有一点,这里执行的顺序是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>标签,只不过四个参数都不能修改

  1. suffixOverrides = null
  2. prefixOverrides = {"AND ","OR ","AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t"}.
  3. prefix = WHERE
  4. 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个参数的内容如下:

  1. suffixOverrides = {","}
  2. prefixOverrides = {"AND ","OR ","AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t"}.
  3. prefix = SET
  4. suffix = null

相关推荐

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——字符串和正则表达式中的反斜杠(&#39;\&#39;)问题详解

在本篇文章里小编给大家整理的是关于Python字符串和正则表达式中的反斜杠('\')问题以及相关知识点,有需要的朋友们可以学习下。在Python普通字符串中在Python中,我们用'\'来转义某些普通...

Python re模块:正则表达式综合指南

Python...

Python中re模块详解(rem python)

在《...

python之re模块(python re模块sub)

re模块一.re模块的介绍1.什么是正则表达式"定义:正则表达式是一种对字符和特殊字符操作的一种逻辑公式,从特定的字符中,用正则表达字符来过滤的逻辑。(也是一种文本模式;)2、正则表达式可以帮助我们...

MySQL、PostgreSQL、SQL Server 数据库导入导出实操全解

在数字化时代,数据是关键资产,数据库的导入导出操作则是连接数据与应用场景的桥梁。以下是常见数据库导入导出的实用方法及代码,包含更多细节和特殊情况处理,助你应对各种实际场景。一、MySQL数据库...

Zabbix监控系统系列之六:监控 mysql

zabbix监控mysql1、监控规划在创建监控项之前要尽量考虑清楚要监控什么,怎么监控,监控数据如何存储,监控数据如何展现,如何处理报警等。要进行监控的系统规划需要对Zabbix很了解,这里只是...

mysql系列之一文详解Navicat工具的使用(二)

本章内容是系列内容的第二部分,主要介绍Navicat工具的使用。若查看第一部分请见:...

取消回复欢迎 发表评论: