Mybatis源码阅读8 --- sql语句的一切(2)

继上篇分析了getById,我们继续分析其他sql语句(多参数,且参数中指定javaType,jdbcType):

    <select id="getById" resultMap="cityResultMap">
        SELECT * FROM city where id = #{id}
    </select>

    <select id="getByNameAndCountryCode" resultType="City">
        SELECT * FROM city where Name = #{name} and CountryCode = #{cCode,javaType=string,jdbcType=VARCHAR}
    </select>

getByNameAndCountryCode和getById相比,SqlSource的构建几乎一样,我们看看解析传参,先看下这个接口:

City getByNameAndCountryCode(@Param(value = "name") String name, @Param(value = "cCode") String countryCode);

参数用@Param注解标识并指定了value,此时在ParamNameResolve中生成的SortedMap为{0,"name";1,"cCode"}(若不用注解,生成的map(jdk>=1.8且setting中userActuaParamName为true):{0,"arg0";1,"arg1"}或者{0,"0";1,"1"},在接口中写的名字被遗忘)生成的param为:


是个Map对象,还加了两个键值对:param1、param2,再看设置参数:


getById走了typeHandlerRegister.hasTypeHandler分支,而现在parameterObject已经是Map对象,就走到else分支,构建MetaObject,并取出propertName(name、cCode,若没有在接口方法加注解,sql语句就只能用#{arg0}或#{0}了)的值。#{}里面加上javaType和jdbcType的作用就是生成ParameterMapping的typeHandler,若没有添加则生成UnKnowTypeHandler单参数和多参数就是这几处不同。那sqlSession.selectList怎么传参数呢?请自己思考下。

MetaObject分析。。。

继续下一个sql(用$标识参数):

    <select id="getByName" resultType="City">
        SELECT * FROM city where Name like '%${name}%'
    </select>

dao方法:

    List<City> getByName(@Param(value = "name") String name);

和之前一样的流程就不说了,这个sql在parseDynamicTags时(构建MixedSqlNode),解析TextSqlNode会判断是否为动态sql,(TextSqlNode判断是否是动态就是判断有无"${}"标识),这里就把TextSqlNode加到MixedSqlNode中了。在使用SqlSource时:


处理sql语句中${name},直接用参数(zhou)替换了name:


假如方法没有@Param注解,OgnlCache.getValue就会报错:


看来对于单个参数而言,如果没有使用@Param注解,参数内容会被直接传递,若有@Param,则参数会被构建为ParamMap对象,key为@Param的value值,value为实际参数值。getBoundSql方法继续往下走,最终还是生成了StaticSqlSource。

动态sql就是在执行某个方法时根据实际参数把DynamicSqlSource转换为StaticSqlSource

静态sql就直接生成了RawSqlSource,内部持有StaticSqlSource

看下生成的boundSql:


后续的执行流程就和getById一样了。

继续下一个sql(返回Map对象):

    <select id="getCityAsMapById" resultType="java.util.Map">
        SELECT * FROM city where id = #{id}
    </select>

这个sql的解析和之前的getById没有区别,本想分析下result的处理,一看还是把我抬走吧,先留着。

继续:

  <select id="getListCDATALtId" resultType="City" parameterType="java.lang.Long">
        select * from city where id  <![CDATA[<=]]> #{maxId}
    </select>

    <select id="getListBetweenIds" resultType="City">
        select * from city where id between #{param1} and #{arg1} order by ${arg2}
    </select>

简要说明下,CDATA是xml的语法,期间的内容会被原样输出,下面三个参数的方法并没有使用@Param注解,那参数就是arg0到argN-1了,mybatis会给你加上param1到paramN

下一个sql(返回Map):

    <select id="getCityMappedById" resultType="City">
        select * from city where id <= #{maxId}
    </select>

解析SqlSource和之前没区别,区别是这个方法返回map对象

@MapKey(value = "id")
Map<Long, City> getCityMappedById(Long maxId);

当然要在方法上注解下map对象的key是什么,value就不用了,value就是sql中的resultType,如何实现的?MapperMethod的MapperSignature对象会解析接口方法:


调用时调用Session.selectMap:


和selectOne一样,还是调用selectList,并对返回的结果处理成map对象。

下一个sql,insert:

    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
        insert into city(Name,CountryCode,District,Population) values (#{name}, #{countryCode}, #{district}, #{population})
    </insert>

对应的接口方法:

    Boolean insert(City city);

返回值为Boolean代表成功与否,也可以为int代表插入数量,参数是City,当然也可以分开写,多参数需要@Param注解哦,insert中使用useGeneratedKey和keyProperty属性用来把自动生成的id赋值给City对象的id字段,同上和之前相同的流程就不分析了(如SqlSource的构建),我们看看不一样的地方:

在构建MappedStatement时会生成Jbdc3KeyGenerator对象而不是NoKeyGenerator。

调用SqlSession的insert方法,内部调用了update方法,CachingExecutor会刷掉待缓存的缓存,然后调用BaseExecutor方法清掉Session缓存

继续看PrepareStatementHandler的update方法:

  @Override
  public int update(Statement statement) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    int rows = ps.getUpdateCount();
    Object parameterObject = boundSql.getParameterObject();
    KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
    keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);
    return rows;
  }

直接把自动生成的id赋值给parameterObject(city)的keyProperty属性(id)了

再提一下设置参数处:


和之前的多参数一样,通过MetaObject获取propertyName的值,注意上图else之前的else if语句,如果parameterObject的类型存在于typeHandlerRegistry,value就是parameterObject,由于typeHandlerRegistry包含了基本类型,那么参数类型如果是非基本类型,或者是自定义的java类,就通过MetaObject来获取对应的propertypName的值。

最后看下返回值,看下MapperMethod这个方法就很清楚了:

  private Object rowCountResult(int rowCount) {
    final Object result;
    if (method.returnsVoid()) {
      result = null;
    } else if (Integer.class.equals(method.getReturnType()) || Integer.TYPE.equals(method.getReturnType())) {
      result = rowCount;
    } else if (Long.class.equals(method.getReturnType()) || Long.TYPE.equals(method.getReturnType())) {
      result = (long)rowCount;
    } else if (Boolean.class.equals(method.getReturnType()) || Boolean.TYPE.equals(method.getReturnType())) {
      result = rowCount > 0;
    } else {
      throw new BindingException("Mapper method '" + command.getName() + "' has an unsupported return type: " + method.getReturnType());
    }
    return result;
  }

update和delete语句是insert的子集,就不再多说了,下面看下动态sql,什么是动态sql?先看下配置:

    <select id="getCityIf" resultType="City">
        select * from city where CountryCode=#{countryCode}
        <if test="district != null">
            and District like #{district}
        </if>
    </select>

动态sql就是说这个sql语句要根据你的参数生成

比如上面的getCityIf,如果district为空:select * from ... #{countryCode}

若不为空:select ... #{countryCode} and District like #{district}

之前已经有过动态sql了,就是当sql语句包含${}时也算是一种动态sql

最终动态sql转换为静态sql,即DynamicSqlSource -> StaticSqlSource

看下代码:


如果是ELEMENT_NODE,就通过handler处理并将生成的SqlNode放入contents:


上面是IfHandler,又递归调用了parseDynamicTags,最终生成的List<SqlNode>:


DynamicSqlSource的MixedSqlNode就像公司的部门一样,是一种组合关系:

Content

    StaticTextSqlNode

    IfSqlNode

        -- StaticTextSqlNode

    StaticTextSqlNode

继续看如何生成BoundSql,先调用rootSqlNode的apply方法生成sql语句,直接看IfSqlNode的apply方法:

  @Override
  public boolean apply(DynamicContext context) {
    if (evaluator.evaluateBoolean(test, context.getBindings())) {
      contents.apply(context);
      return true;
    }
    return false;
  }

满足条件则会调用contents.apply,contents是IfSqlNode内部的SqlNode,看下最终生成的BoundSql:


BoundSql中有sql、sql?的参数信息,实际参数,动态sql就这些,再看几个熟悉下,下一个bind:

    <select id="getCityIfLike" resultType="City">
        <bind name="likeDistrict" value="'%' + district + '%'" />
        select * from city where CountryCode=#{countryCode}
        <if test="district != null">
            and District like #{likeDistrict}
        </if>
    </select>

bind的作用就是对参数做一些额外的格式化,看下bindHandler在hanlerNode时生成的VarDeclSqlNode,apply方法直接把name和value放到context的bindings中:


还记得MappedStatement.getBoundSql最后一步吗?把context的binds设置到BoundSql的additionalParameter中,而在DefaultParameterHandler的setParameters方法中,取值会先去boundSql的additionalParameter查一下,这里发现有likeDistrict,就直接用它的value了。

下一个choose:

    <select id="getCityChoose" resultType="City" parameterType="City">
        select * from city where CountryCode=#{countryCode}
        <choose>
            <when test="name != null">
                and Name = #{name}
            </when>
            <when test="district != null">
                and District = #{district}
            </when>
            <otherwise>
                and Name = 'Peking'
            </otherwise>
        </choose>
    </select>

看下类定义就全明白了:

public class ChooseSqlNode implements SqlNode {
  private final SqlNode defaultSqlNode;
  private final List<SqlNode> ifSqlNodes;

  public ChooseSqlNode(List<SqlNode> ifSqlNodes, SqlNode defaultSqlNode) {
    this.ifSqlNodes = ifSqlNodes;
    this.defaultSqlNode = defaultSqlNode;
  }

  @Override
  public boolean apply(DynamicContext context) {
    for (SqlNode sqlNode : ifSqlNodes) {
      if (sqlNode.apply(context)) {
        return true;
      }
    }
    if (defaultSqlNode != null) {
      defaultSqlNode.apply(context);
      return true;
    }
    return false;
  }
}

下三个<where><trim><set>:

    <select id="getCityWhere" resultType="City" parameterType="City">
        select * from city
        <where>
            <if test="countryCode != null">CountryCode=#{countryCode}</if>
            <if test="name != null">and Name=#{name}</if>
            <if test="district != null">and District=#{district}</if>
        </where>
    </select>

    <select id="getCityTrim" resultType="City" parameterType="City">
        select * from city
        <trim prefix="WHERE" prefixOverrides="AND |OR ">
            <if test="countryCode != null">CountryCode=#{countryCode}</if>
            <if test="name != null">and Name=#{name}</if>
            <if test="district != null">and District=#{district}</if>
        </trim>
    </select>

    <update id="updateSet" parameterType="City">
        update city
        <set>
            <if test="name != null">Name=#{name},</if>
            <if test="countryCode != null">CountryCode=#{countryCode},</if>
            <if test="district != null">District=#{district},</if>
            <if test="population != null">Population=#{population}</if>
        </set>
        where ID = #{id}
    </update>

    <update id="updateTrim" parameterType="City">
        update city
            <trim prefix="SET" suffixOverrides=",">
                    <if test="name != null">Name=#{name},</if>
                    <if test="countryCode != null">CountryCode=#{countryCode},</if>
                    <if test="district != null">District=#{district},</if>
                    <if test="population != null">Population=#{population}</if>
            </trim>
        where ID = #{id}
    </update>

WhereSqlNode和SetSqlNode基础TrimSqlNode,使用where、set标签需要给sql加上where、set,where需要去掉紧随where的and、or,set需要去掉最后那个逗号

WhereSqlNode:

public class WhereSqlNode extends TrimSqlNode {

  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:把WHERE加到sql语句最前面,且紧随where的prefixList都给我去掉

SetSqlNode:

public class SetSqlNode extends TrimSqlNode {

  private static List<String> suffixList = Arrays.asList(",");

  public SetSqlNode(Configuration configuration,SqlNode contents) {
    super(configuration, contents, "SET", null, null, suffixList);
  }

}

告诉TrimSqlNode,把SET加到sql语句最前面,sql最后如果是suffixList,去掉他们

TrimSqlNode老老实实干活喽:


最后一个foreach:

 <!-- index是集合索引,item为该索引的元素 -->
    <select id="getCityForEachList" resultType="City" parameterType="java.util.List">
        select * from city where ID in
        <foreach item="item" index="index" collection="list" open="(" separator="," close=")">
            #{item},#{index}
        </foreach>
    </select>

    <!-- index是集合索引,item为该索引的元素 -->
    <select id="getCityForEachArray" resultType="City">
        select * from city where ID in
        <foreach item="item" index="index" collection="array" open="(" separator="," close=")">
            #{item},#{index}
        </foreach>
    </select>

    <!-- index是集合索引,item为该索引的元素 -->
    <select id="getCityForEachSet" resultType="City" parameterType="java.util.Set">
        select * from city where ID in
        <foreach item="item" index="index" collection="collection" open="(" separator="," close=")">
            #{item},#{index}
        </foreach>
    </select>

    <select id="getCityForEachMap" resultType="City">
        select * from city where ID in
        <foreach item="item" index="index" collection="ids" open="(" separator="," close=")">
            #{item},#{index}
        </foreach>
    </select>

foreach常用语批量insert,或者select ... in (arg1,arg2,arg3....),参数可以是Array、List、Set、Map,若参数为map,需要@Params注解喽,foreach标签中item代表集合中的对象,index代表集合的索引号,collection代表你传入的是collection或者array(这一点我不太明白),open和close会在foreach之前、之后加上字符(一般为左右括号),seperate为每个迭代间插入字符(一般为逗号了),传参调用后,比如传入的是ArrayList,DefaultSqlSession会wrapCollection:

  private Object wrapCollection(final Object object) {
    if (object instanceof Collection) {
      StrictMap<Object> map = new StrictMap<Object>();
      map.put("collection", object);
      if (object instanceof List) {
        map.put("list", object);
      }
      return map;
    } else if (object != null && object.getClass().isArray()) {
      StrictMap<Object> map = new StrictMap<Object>();
      map.put("array", object);
      return map;
    }
    return object;
  }

若是collection(array,set)则key为collection,若是list对象,同样添加key为list的Entry,若是数组则key为array,若是map,好吧,直接return,我们先看下mixedSqlNode.apply:


继续到ForEachSqlNode.apply方法:

  @Override
  public boolean apply(DynamicContext context) {
    Map<String, Object> bindings = context.getBindings();
    final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
    if (!iterable.iterator().hasNext()) {
      return true;
    }
    boolean first = true;
    applyOpen(context);
    int i = 0;
    for (Object o : iterable) {
      DynamicContext oldContext = context;
      if (first || separator == null) {
        context = new PrefixedContext(context, "");
      } else {
        context = new PrefixedContext(context, separator);
      }
      int uniqueNumber = context.getUniqueNumber();
      // Issue #709 
      if (o instanceof Map.Entry) {
        @SuppressWarnings("unchecked") 
        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);
        applyItem(context, o, uniqueNumber);
      }
      contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
      if (first) {
        first = !((PrefixedContext) context).isPrefixApplied();
      }
      context = oldContext;
      i++;
    }
    applyClose(context);
    context.getBindings().remove(item);
    context.getBindings().remove(index);
    return true;
  }
根据collectionExpression(list or array or 自定义的ids)来获取iterable对象,collection属性的作用应该就是这个吧 惊恐

后续方法:

1、先拿到迭代对象

2、应用open符号,如左括号

3、然后就开始遍历,

    3.1、先保存一个oldContext

    3.2、若第一次遍历,或分隔符为空,new 一个PrefixContext,且构造参数为空字符串,否则构造函数为分隔符

    3.3、bind索引及集合内容,key为item、index,且构造了另外一个key:__frch_index_0,__frch_item_0,value为下标索引及该索引对应的值

    3.4、new FilteredDynamicContext,参数为context(这里是PrefixContext了),调用sqlNode.apply方法,我们的foreachSqlNode的MixedSqlNode是这样:


FilteredDynamicContext的appendSql就是把#{item}及#{index}替换为#{__frch__item_0} #{__frch_index_0},然后调用了delegate.appendSql,就是PrefixContext的appendSql

    @Override
    public void appendSql(String sql) {
      if (!prefixApplied && sql != null && sql.trim().length() > 0) {
        delegate.appendSql(prefix);
        prefixApplied = true;
      }
      delegate.appendSql(sql);
    }

分隔符就是这样加进去的

    3.5、刚才的是3.4的东东哦,有点多,下一步就是把first设置为false,把oldContext赋值回context(原来的context被装修了两下,想通过装饰后的context拿到装饰前的context也是可以的)

4、应用close符号,去掉bing的item和index,最后看下ForEachSqlNode的apply方法生成的context:


然后及通过SqlSourceParse用?替掉#{}里的内容,生成StaticSqlSource。foreach就是这些了,不过刚才用的是集合是基本类型,我们看下批量插入:

    <insert id="insertBatch" useGeneratedKeys="true" keyProperty="id">
        insert into city (Name,CountryCode,District,Population) values
        <foreach item="city" index="index" collection="collection" separator=",">
            (#{city.name}, #{city.countryCode}, #{city.district}, #{index})
        </foreach>
    </insert>

使用了city.name,生成的bind还是一样key:__frch_city_0 ... n, value: city对象,sql的#{city.name}变为#{__frch_city_0.name},

如何给参数赋值呢?下图你debug到的话应该很清楚了:


调用之前是这里:

  public Object getValue(String name) {
    PropertyTokenizer prop = new PropertyTokenizer(name);
    if (prop.hasNext()) {
      MetaObject metaValue = metaObjectForProperty(prop.getIndexedName());
      if (metaValue == SystemMetaObject.NULL_META_OBJECT) {
        return null;
      } else {
        return metaValue.getValue(prop.getChildren());
      }
    } else {
      return objectWrapper.get(prop);
    }
  }
PropertyTokenizer很有意思,它会把点后面的当做child,之前的当name,还有强大的MetaObject,所有对象通吃!
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值