Mybatis sql解析过程

一、Mybatis解析sql的时机

Mybatis对于用户在XXMapper.xml文件中配置的sql解析主要分为2个时机

静态sql:程序启动的时候解析

动态sql:用户进行查询等sql相关操作的时候解析

二、静态sql、动态sql

1、什么是静态sql,动态sql?

如果select|insert|update|delete标签体内包含XML标签或者select|insert|update|delete标签体内的sql文本中包含${}参数占位符则为动态sql,否则为静态sql。

如下面的2个sql中,第一个为动态sql,第二个为静态sql

<select id="selectUser" parameterType="com.fit.bean.User" resultType="com.fit.bean.User" useCache="true">
	select id, name from tab_user where id = ${id}
	<if test="name!=null and name!=''">
	and name=#{name}
	</if>
	and 1 = 1
</select>

<select id="selectUserById" parameterType="int" resultType="com.fit.bean.User" useCache="true">
	select id, name from tab_user where id = #{id}
</select>

2、静态sql和动态sql的选择

由于静态sql是在应用启动的时候就解析,而动态sql是在执行该sql相关操作的时候才根据传入的参数进行解析的,所以静态sql效率会比动态sql好。

Static SqlSource is faster than DynamicSqlSource because mappings are calculated during startup.

PS:此处只针对常见的Mybatis的sql脚本写法,通过<script></script>传入sql执行的方式暂不讨论。

三、sql解析过程

先看一下Mybatis的sql解析过程涉及到下面的几个主要对象(关键类:MappedStatement、SqlSource、BoundSql)



其中DynamicSqlSource的解析过程涉及到动态sql节点(关键类:SqlNode)的解析,涉及到的类(以if标签为例,只画了解析过程中的几个主要的类,SqlNode的其他子类如ChooseSqlNode、ForEachSqlNode、TrimSqlNode、WhereSqlNode等没有画出来)如下



先用一个图表示解析结果如下:


再结合源码看下解析过程,Myabatis解析每一个select|insert|update|delete标签体成一个MappedStatement对象,里面保存了一个SqlSource对象的引用。

Mybatis解析select|insert|update|delete标签体内配置的sql是通过XMLScriptBuilder类的parseScriptNode方法实现,

public SqlSource parseScriptNode() {
  //解析select|insert|update|delete标签体,生成一系列的SqlNode
  List<SqlNode> contents = parseDynamicTags(context);
  //混合的SqlNode,其实就是保存了一个List<SqlNode>类型的属性
  MixedSqlNode rootSqlNode = new MixedSqlNode(contents);
  SqlSource sqlSource = null;
  if (isDynamic) {
    //动态SqlSource
    sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
  } else {
    //静态SqlSource
    sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
  }
  return sqlSource;
}

首先看下parseDynamicTags方法

List<SqlNode> parseDynamicTags(XNode node) {
  List<SqlNode> contents = new ArrayList<SqlNode>();
  NodeList children = node.getNode().getChildNodes();
  for (int i = 0; i < children.getLength(); i++) {
    XNode child = node.newXNode(children.item(i));
    if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
      String data = child.getStringBody("");
      //TextSqlNode解析判断是否是动态sql
      TextSqlNode textSqlNode = new TextSqlNode(data);
      if (textSqlNode.isDynamic()) {
        contents.add(textSqlNode);
        isDynamic = true;
      } else {
        contents.add(new StaticTextSqlNode(data));
      }
    } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
      String nodeName = child.getNode().getNodeName();
      //不同的标签用对应的NodeHandler处理
      NodeHandler handler = nodeHandlers(nodeName);
      if (handler == null) {
        throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
      }
	 //如果还有动态的标签,递归调用parseDynamicTags
      handler.handleNode(child, contents);
      isDynamic = true;
    }
  }
  return contents;
}
它的作用就是把select|insert|update|delete标签体解析成一个个的SqlNode节点,并判断出该标签是静态sql还是动态sql,如果是动态的生成DynamicSqlSource,如果是静态sql,就生成RawSqlSource。而RawSqlSource就是包装了一个StaticSqlSource,可以看下RawSqlSource构造方法的实现:

public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
    this(configuration, getSql(configuration, rootSqlNode), parameterType);
  }


  public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> clazz = parameterType == null ? Object.class : parameterType;
    //解析其中的#{},替换成预编译sql中的? 并保存参数映射
    sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<String, Object>());
  }


  private static String getSql(Configuration configuration, SqlNode rootSqlNode) {
    DynamicContext context = new DynamicContext(configuration, null);
    //StaticTextSqlNode的apply方法就是把append静态sql文本
    rootSqlNode.apply(context);
    return context.getSql();
  }

1、静态sql解析

静态sql的解析就是替换sql文本中的#{}参数成?,即生成最终可以预编译的sql,并把参数相关信息保存成ParameterMapping,包括参数名,数据类型,以及根据数据类型获取对应的TypeHandler。

TypeHanler的作用就是在执行预编译sql的时候设置参数值,决定参数设值是用prepareStatement.setInt()还是prepareStatement.setString()等。

2、动态sql解析

动态sql的解析是在执行db操作的调用MappedStatement方法的getBoundSql方式时进行解析的

public BoundSql getBoundSql(Object parameterObject) {
//SqlSource中生成BoundSql,如果是DynamicSqlSource则借助ognl根据入参替换${}成参数值
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings == null || parameterMappings.isEmpty()) {
  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

我们看下<if></if>标签的解析

<if test="name!=null and name!=''">
and name=#{name}
</if>
它会被解析成IfSqlNode

public class IfSqlNode implements SqlNode {
  private ExpressionEvaluator evaluator;
  private String test;
  private SqlNode contents;

  public IfSqlNode(SqlNode contents, String test) {
    this.test = test;
    this.contents = contents;
    this.evaluator = new ExpressionEvaluator();
  }

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

}

它的apply方法就是根据入参计算name!=null and name!=''表达式的值,如果是true,则调用if标签体内的sqlNode的apply方法,and name=#{name}是StaticTextSqlNode,则替换#{name}成?后直接append,if标签中计算表达式的值借助了ognl来实现。

ognl是对象图导航语言,主要作用就是根据参数名直接取对象/级联对象的属性值,它也可以计算ognl表达式的值,如上面的name!=null and name!=''表达式。

最终DynamicSqlSource会被解析成只包含#{}的StaticSqlSource,静态SqlSource再获取可以直接预编译的sql。

四、sql执行

执行查询的时候真正调用的是SimpleExecutor的doQuery方法

public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
  Statement stmt = null;
  try {
    Configuration configuration = ms.getConfiguration();
    //根据sql标签配置的StatementType生成对应的StatementHandler,不配置的话默认是PreparedStatementHandler,即执行预编译sql。
    StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
    //预编译sql,并且给参数赋值,即根据解析的ParameterMapping一个一个进行参数设值
    stmt = prepareStatement(handler, ms.getStatementLog());
    //执行查询、并解析结果集返回
    return handler.<E>query(stmt, resultHandler);
  } finally {
    closeStatement(stmt);
  }
}

prepareStatement方法就是预编译sql,对不同的参数根据类型调用不同的TypeHandler进行preparestatement设值。

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog);
    stmt = handler.prepare(connection);
    handler.parameterize(stmt);
    return stmt;
  }

sql解析的大概流程就是这样


五、#{}和${}的区别

  • 所有的#{}标签都会替换成?,而${}在sql解析的过程中会根据参数使用ognl直接替换成对应的参数值,如果参数中name是"jack",则sql中会直接替换成name=jack,sql执行会报错。
  • 如果传入的参数是基本数据类型,则参数占位符不能用${},因为ognl取参数值的时候会对传入的参数调用占位符中对应的属性,导致基本数据类型不可能有该属性而报错。如果sql只想传一个参数又是基本数据类型用#{}。
  • 如果User对象的id为int类型,id值为0,ognl对user对象进行表达式id!=null and id!=''计算的时候会返回false,if便签里面的sql就不会被append。所以基本数据类型int不要用 !=''做判断
  • 如果标签中指定StatementType="STATEMENT",sql标签体内包含#{},会被解析成?而不进行参数设值,sql执行报错,默认StatementType="PREPARED"
  • #{}可以防止sql注入,也就是预编译sql的好处


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值