Mybatis SQL解析过程

from: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


  1. <select id="selectUser" parameterType="com.fit.bean.User" resultType="com.fit.bean.User" useCache="true">

  2.     select id, name from tab_user where id = ${id}

  3.     <if test="name!=null and name!=''">

  4.     and name=#{name}

  5.     </if>

  6.     and 1 = 1

  7. </select>


  8. <select id="selectUserById" parameterType="int" resultType="com.fit.bean.User" useCache="true">

  9.     select id, name from tab_user where id = #{id}

  10. </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方法实现,


  1. public SqlSource parseScriptNode() {

  2.   //解析select|insert|update|delete标签体,生成一系列的SqlNode

  3.   List<SqlNode> contents = parseDynamicTags(context);

  4.   //混合的SqlNode,其实就是保存了一个List<SqlNode>类型的属性

  5.   MixedSqlNode rootSqlNode = new MixedSqlNode(contents);

  6.   SqlSource sqlSource = null;

  7.   if (isDynamic) {

  8.     //动态SqlSource

  9.     sqlSource = new DynamicSqlSource(configuration, rootSqlNode);

  10.   } else {

  11.     //静态SqlSource

  12.     sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);

  13.   }

  14.   return sqlSource;

  15. }



首先看下parseDynamicTags方法


  1. List<SqlNode> parseDynamicTags(XNode node) {

  2.   List<SqlNode> contents = new ArrayList<SqlNode>();

  3.   NodeList children = node.getNode().getChildNodes();

  4.   for (int i = 0; i < children.getLength(); i++) {

  5.     XNode child = node.newXNode(children.item(i));

  6.     if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {

  7.       String data = child.getStringBody("");

  8.       //TextSqlNode解析判断是否是动态sql

  9.       TextSqlNode textSqlNode = new TextSqlNode(data);

  10.       if (textSqlNode.isDynamic()) {

  11.         contents.add(textSqlNode);

  12.         isDynamic = true;

  13.       } else {

  14.         contents.add(new StaticTextSqlNode(data));

  15.       }

  16.     } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628

  17.       String nodeName = child.getNode().getNodeName();

  18.       //不同的标签用对应的NodeHandler处理

  19.       NodeHandler handler = nodeHandlers(nodeName);

  20.       if (handler == null) {

  21.         throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");

  22.       }

  23. <span style="white-space:pre">  </span> //如果还有动态的标签,递归调用parseDynamicTags

  24.       handler.handleNode(child, contents);

  25.       isDynamic = true;

  26.     }

  27.   }

  28.   return contents;

  29. }



它的作用就是把select|insert|update|delete标签体解析成一个个的SqlNode节点,并判断出该标签是静态sql还是动态sql,如果是动态的生成DynamicSqlSource,如果是静态sql,就生成RawSqlSource。而RawSqlSource就是包装了一个StaticSqlSource,可以看下RawSqlSource构造方法的实现:


  1. public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {

  2.     this(configuration, getSql(configuration, rootSqlNode), parameterType);

  3.   }

  4.   public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {

  5.     SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);

  6.     Class<?> clazz = parameterType == null ? Object.class : parameterType;

  7.     //解析其中的#{},替换成预编译sql中的? 并保存参数映射

  8.     sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<String, Object>());

  9.   }



  10.   private static String getSql(Configuration configuration, SqlNode rootSqlNode) {

  11.     DynamicContext context = new DynamicContext(configuration, null);

  12.     //StaticTextSqlNode的apply方法就是把append静态sql文本

  13.     rootSqlNode.apply(context);

  14.     return context.getSql();

  15.   }

1、静态sql解析

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

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

2、动态sql解析

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


  1. public BoundSql getBoundSql(Object parameterObject) {

  2. //SqlSource中生成BoundSql,如果是DynamicSqlSource则借助ognl根据入参替换${}成参数值

  3. BoundSql boundSql = sqlSource.getBoundSql(parameterObject);

  4. List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();

  5. if (parameterMappings == null || parameterMappings.isEmpty()) {

  6.   boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);

  7. }

  8. // check for nested result maps in parameter mappings (issue #30)

  9. for (ParameterMapping pm : boundSql.getParameterMappings()) {

  10.   String rmId = pm.getResultMapId();

  11.   if (rmId != null) {

  12.     ResultMap rm = configuration.getResultMap(rmId);

  13.     if (rm != null) {

  14.       hasNestedResultMaps |= rm.hasNestedResultMaps();

  15.     }

  16.   }

  17. }

  18. return boundSql

我们看下<if></if>标签的解析
  1. <if test="name!=null and name!=''">

  2. and name=#{name}

  3. </if>

它会被解析成IfSqlNode

  1. public class IfSqlNode implements SqlNode {

  2.   private ExpressionEvaluator evaluator;

  3.   private String test;

  4.   private SqlNode contents;


  5.   public IfSqlNode(SqlNode contents, String test) {

  6.     this.test = test;

  7.     this.contents = contents;

  8.     this.evaluator = new ExpressionEvaluator();

  9.   }


  10.   @Override

  11.   public boolean apply(DynamicContext context) {

  12.     if (evaluator.evaluateBoolean(test, context.getBindings())) {

  13.       contents.apply(context);

  14.       return true;

  15.     }

  16.     return false;

  17.   }


  18. }



它的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方法
  1. public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {

  2.   Statement stmt = null;

  3.   try {

  4.     Configuration configuration = ms.getConfiguration();

  5.     //根据sql标签配置的StatementType生成对应的StatementHandler,不配置的话默认是PreparedStatementHandler,即执行预编译sql。

  6.     StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);

  7.     //预编译sql,并且给参数赋值,即根据解析的ParameterMapping一个一个进行参数设值

  8.     stmt = prepareStatement(handler, ms.getStatementLog());

  9.     //执行查询、并解析结果集返回

  10.     return handler.<E>query(stmt, resultHandler);

  11.   } finally {

  12.     closeStatement(stmt);

  13.   }

  14. }

prepareStatement方法就是预编译sql,对不同的参数根据类型调用不同的TypeHandler进行preparestatement设值。
  1. private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {

  2.     Statement stmt;

  3.     Connection connection = getConnection(statementLog);

  4.     stmt = handler.prepare(connection);

  5.     handler.parameterize(stmt);

  6.     return stmt;

  7.   }

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的好处