(五)Mybatis Sql如何解析执行(1)--MyBatis源码解析

通过前面一些篇幅,我们整理了sqlSession和executor关系之后,接下来就要看看具体一条sql是怎么被解析执行了,先看下query代码。

对应的mapper配置文件我也补上,后续会将代码打包发布到github上

<mapper namespace="com.mybatis.demo.User">
    <resultMap type="com.mybatis.demo.User" id="detailUserResultMap">
        <constructor>
            <idArg column="user_id" javaType="String"/>
            <arg column="user_name" javaType="String"/>
        </constructor>
        <result property="password" column="user_pwd" />
        <result property="type" column="user_type" javaType="com.mybatis.demo.UserType" />
        <result property="svcnum" column="svc_num" />
        <association property="cust" javaType="com.mybatis.demo.Cust">
            <id property="id" column="cust_id"/>
            <result property="custname" column="cust_name"/>
            <result property="certNo" column="cert_no"/>
        </association>
        <collection property="accts" ofType="com.mybatis.demo.Acct">
            <id property="id" column="acct_id" />
            <result property="payName" column="pay_name"/>
            <result property="bankNo" column="bank_no"/>
        </collection>
    </resultMap>
    <select id="selectUserDetail" resultMap="detailUserResultMap">
        <![CDATA[
			select user_id,user_name from tf_f_user a where a.user_id=#{userId}
		]]>
    </select>
</mapper>

首先来看下DefaultSqlSession的selectList方法

public <E> List<E> selectList(String statement, Object parameter) {
	//RowBounds表示查询的范围,一般在分页时用到
	return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
	try {
		//从Configuration获取一个MappedStatement配置
		MappedStatement ms = configuration.getMappedStatement(statement);
		//直接调用executor.query()方法
		List<E> result = executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
		return result;
	}catch (Exception e) {
		throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
	}finally {
		ErrorContext.instance().reset();
	}
}

从源码看selectList方法,是通过executor来完成查询的,那么BaseExecutor的query方法大家是否还记得那?

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, 
ResultHandler resultHandler) throws SQLException {
    //获取一个BoundSql,这个BoundSql的获取过程就是本节要详细讨论的
    BoundSql boundSql = ms.getBoundSql(parameter);
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

然而BoundSql 才是真正获取配置文件sql语句的重点。来看下它的源码吧小伙伴

/**
 * An actual SQL String got form an {@link SqlSource} after having processed any dynamic content.
 * The SQL may have SQL placeholders "?" and an list (ordered) of an parameter mappings 
 * with the additional information for each parameter (at least the property name of the input object to read 
 * the value from). 
 * </br>
 * Can also have additional parameters that are created by the dynamic language (for loops, bind...).
 */
/**
 * @author Clinton Begin
 */
public class BoundSql {
  //经过处理的sql,这个sql已经可以被数据库执行了
  private String sql;
  //sql中的参数映射,只是映射,没有包含实际的值
  private List<ParameterMapping> parameterMappings;
  //客户端执行sql时传入的参数
  private Object parameterObject;
 
  //暂时不讨论
  private Map<String, Object> additionalParameters;
  //暂时不讨论
  private MetaObject metaParameters;
 
  public BoundSql(Configuration configuration, String sql, List<ParameterMapping> 
                                        parameterMappings, Object parameterObject) {
    this.sql = sql;
    this.parameterMappings = parameterMappings;
    this.parameterObject = parameterObject;
    this.additionalParameters = new HashMap<String, Object>();
    this.metaParameters = configuration.newMetaObject(additionalParameters);
  }
 
  public String getSql() {
    return sql;
  }
 
  public List<ParameterMapping> getParameterMappings() {
    return parameterMappings;
  }
 
  public Object getParameterObject() {
    return parameterObject;
  }
 
  public boolean hasAdditionalParameter(String name) {
    return metaParameters.hasGetter(name);
  }
 
  public void setAdditionalParameter(String name, Object value) {
    metaParameters.setValue(name, value);
  }
 
  public Object getAdditionalParameter(String name) {
    return metaParameters.getValue(name);
  }
}

从源代码可以看出,BoundSql只是一个简单的java对象,有两个属性比较重要

  1. sql:从解析时可以看出这个sql不是配置文件中的sql,这个sql已经经过了处理(如:占用位符的处理、动态语句的解析if、foreach等待)
  2. parameterMappings:sql对应的参数列表
  3. 大家可以自己跟踪下,看看是不是已经解析完的sql,O(∩_∩)O!

在这我就不举例说明了,因为只要用过mybatis的小伙伴都知道参数通过<if test=></if>标签可以处理哪些参数可以解析到执行sql

我们接下来直接看MappedStatement.getBoundSql()方法

public BoundSql getBoundSql(Object parameterObject) {
    //通过sqlSource对象获取
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
   
    //parameterMap一般不会配置,如下内容不讨论
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings == null || parameterMappings.size() <= 0) {
      boundSql = new BoundSql(configuration, boundSql.getSql(), 
                  parameterMap.getParameterMappings(), parameterObject);
    }
    // check for nested result maps in parameter mappings (issue #30)
    for (ParameterMapping pm : boundSql.getParameterMappings()) {
      String rmId = pm.getResultMapId();
      if (rmId != null) {
        ResultMap rm = configuration.getResultMap(rmId);
        if (rm != null) {
          hasNestedResultMaps |= rm.hasNestedResultMaps();
        }
      }
    }
    return boundSql;
}

再这就需要插个小插曲了,不知道大家知道不知道sqlSource是怎么被创建的吗?

public SqlSource parseScriptNode() {
    List<SqlNode> contents = parseDynamicTags(context);
    MixedSqlNode rootSqlNode = new MixedSqlNode(contents);
    SqlSource sqlSource = null;
    if (isDynamic) {
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
}

DynamicSqlSource.getBoundSql()方法

DynamicContext可以看成是一个sql的容器,sqlNode的apply()方法会往这个容器上加sql.

public class DynamicSqlSource implements SqlSource {
 
  private Configuration configuration;
  private SqlNode rootSqlNode;
  public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
    this.configuration = configuration;
    this.rootSqlNode = rootSqlNode;
  }
  public BoundSql getBoundSql(Object parameterObject) {
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    //sqlNode使用组合模式实现,他有多个SqlNode对象
    //每个SqlNode的apply方法调用时,都为将sql加到context中,最终通过context.getSql()得到完整的sql
    rootSqlNode.apply(context);
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
      boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
    }
    return boundSql;
  }
}

DynamicContext动态上下文

//参数上下文,ContextMap为一个Map
private final ContextMap bindings;
//sql,sqlNode中的apply()方法调用了appendSql(text)方法,最终会将sql保存在这个属性中
private final StringBuilder sqlBuilder = new StringBuilder();
public void appendSql(String sql) {
    sqlBuilder.append(sql);
    sqlBuilder.append(" ");
} 
public String getSql() {
    return sqlBuilder.toString().trim();
}

再看看参数上下文

//ContextMap  是DynamicContext的一个内部类
static class ContextMap extends HashMap<String, Object> {
        private static final long serialVersionUID = 2977601501966151582L;
        //这个对运行时的参数进行了包装
        private MetaObject parameterMetaObject;

        public ContextMap(MetaObject parameterMetaObject) {
            this.parameterMetaObject = parameterMetaObject;
        }
        public Object put(String key, Object value) {
            return super.put(key, value);
        }

        //这个方法才是最重要的
        public Object get(Object key) {
            String strKey = (String)key;
            //如果自身的map里
            if(super.containsKey(strKey)) {
                return super.get(strKey);
            } else if(this.parameterMetaObject != null) {
                //从参数里找
                Object object = this.parameterMetaObject.getValue(strKey);
                return object;
            } else {
                return null;
            }
        }
}

咱们来举个例子来说明contextMap是如何使用的,其中metaObject暂不做解释。

参数为Map类型
Map paraMap=new HashMap();
paraMap.put("userId","12341234");
paraMap.put("userName","ashan");
List<User> list=sqlSession.selectList("dao.selectUser",paraMap);

参数以普通对象
User user=new User();
user.setUserId("12341234");
user.setUserName("ashan");
List<User> list=sqlSession.selectList("dao.selectUser",user);

以上两种方式是最常见的参数设置方式,调用ContextMap.get("userId")方法之后,都能得到"12341234"!这就是ContextMap提供的功能

SqlSource与SqlNode

下面详细分析apply()方法。例如:DynamicSqlSource是从如下配置加载的

select user_id,user_name,user_type,cust_id
from tf_f_user a where a.user_id=#{userId} 
<if test="userName!=null"> 
	and user_name=${userName} 
</if>

这个DynamicSqlSoure的结构如下(以上面的SQL为例),

结合例子说明一下sql在sqlNode中是怎么分布的

  1. StaticTextSqlNode1:保存了"select user_id,user_name,user_type,cust_id"
  2. StaticTextSqlNode2:保存了"from tf_f_user a"
  3. TextSqlNode3:保存了"where a.user_id=#{userId}",同时标识为动态的,因为他有占位符
  4. StaticTextSqlNode4:保存了"and"
  5. TextSqlNode5:保存了"user_name=#{userName}"
  6. IfSqlNode:保存了其test属性值,StaticTextSqlNode4和TextSqlNode5是否加入的context中也是由其控制的

接下来看看每一种SqlNode是怎么解析sql并生成parameterMapping的

StaticTextSqlNode.apply()方法

public boolean apply(DynamicContext context) {
    context.appendSql(text);
    return true;
}

只是简单的把对应的test追加到context中。

所以StaticTextSqlNode1和StaticTextSqlNode2的apply方法执行后,DynamicContext中的sql内容为:

select  user_id,user_name,user_type,cust_id from tf_f_user a

TextSqlNode.apply()方法

public boolean apply(DynamicContext context) {
    //GenericTokenParser为一个占用符解析器
    //BindingTokenParsery为一个TohenHandler:解析具体的占位符
    GenericTokenParser parser = createParser(new BindingTokenParser(context));
    context.appendSql(parser.parse(text));
    return true;
}
private GenericTokenParser createParser(TokenHandler handler) {
    //解析${tab_name}这种占位符,注意不是这种#{propertyName}
    return new GenericTokenParser("${", "}", handler);
}
再看看GenericTokenParser.parse()方法:
public String parse(String text) {
	StringBuilder builder = new StringBuilder();
	if (text != null && text.length() > 0) {
		char[] src = text.toCharArray();
		int offset = 0;
		int start = text.indexOf(openToken, offset);
		while (start > -1) {
			if (start > 0 && src[start - 1] == '\\') {
				// the variable is escaped. remove the backslash.
				builder.append(src, offset, start - 1).append(openToken);
				offset = start + openToken.length();
			} else {
				int end = text.indexOf(closeToken, start);
				if (end == -1) {
					builder.append(src, offset, src.length - offset);
					offset = src.length;
				} else {
					builder.append(src, offset, start - offset);
					offset = start + openToken.length();
					String content = new String(src, offset, end - offset);
					//关键是这句,调用了handler.handleToken()方法
					builder.append(handler.handleToken(content));
					offset = end + closeToken.length();
				}
			}
			start = text.indexOf(openToken, offset);
		}
		if (offset < src.length) {
			builder.append(src, offset, src.length - offset);
		}
	}
	return builder.toString();
}

认真分析上面的代码,最关键的是调用了handler.handleToken(content)方法

如果text为:select ${primary_key},${col_name} from ${tab_name),那么handler.handleToken()方法会被调用三次,分别为:

  1. handler.handleToken("primary_key")
  2. handler.handleToken("col_name")
  3. handler.handleToken("tab_name")

再来看BindingTokenParser.handleToken()方法

public String handleToken(String content) {
      Object parameter = context.getBindings().get("_parameter");
      if (parameter == null) {
        context.getBindings().put("value", null);
      } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
        context.getBindings().put("value", parameter);
      }
      //从ContextMap中取出content对应的值返回
      Object value = OgnlCache.getValue(content, context.getBindings());
      return (value == null ? "" : String.valueOf(value)); // issue #274 return "" instead of "null"
}

从上面可以看到TextSqlNode.apply(),只会处理"${}"这种占位符,而不会处理这种占位符:"#{}"

所以当TextSqlNode3.apply()执行完成之后,DynamicContext中的sql内容为:

select  user_id,user_name,user_type,cust_id from tf_f_user a where user_id=#{userId}

IfSqlNode.apply()方法

public Boolean apply(DynamicContext context) {
    //动态执行test属性中表达式,如果返回true,才会执行对应的SqlNode.apply()方法
    if (evaluator.evaluateBoolean(test, context.getBindings())) {
        contents.apply(context);
        return true;
    }
    return false;
}

结合上例,当IfSqlNode.apply()方法执行后,有两种情况:

如果参数中的userName不为空的话,DynamicContext中的sql内容为:

select  user_id,user_name,user_type,cust_id from tf_f_user a 
where user_id=#{userId} and user_name=#{userName}

如果参数呻的userName为空的话,DynamicContext中的sql内容为:

select  user_id,user_name,user_type,cust_id from tf_f_user a where user_id=#{userId}

ForEachSqlNode和ChooseSqlNode的实现原理跟IfSqlNode实现差不多,感兴趣的小伙伴可以自行跟踪代码看下。

SqlNode.apply()方法生成的sql也只是半成品,并没有处理"#{}"占位符!这个占位符的处理后续再分析。

猜你喜欢

转载自blog.csdn.net/ikownyou/article/details/81387221