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对象的引用。

通过XMLStatementBuilder类的parseStatementNode方法解析xml

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

————————————————
版权声明:本文为CSDN博主「bootstrap8」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u012387062/article/details/55005414

posted on   yuechuan  阅读(1187)  评论(0编辑  收藏  举报

相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· .NET Core 中如何实现缓存的预热?
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

统计

点击右上角即可分享
微信分享提示