一、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.");
- }
- <span style="white-space:pre"> </span> //如果还有动态的标签,递归调用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>
- 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的好处