MyBatis运行
生成可执行SQL
生成可执行的SQL,指的是可以交给JDBC执行的SQL语句。
在执行的时候,通过调用SqlSource#getBoundSql()生成的,接下来我们再分析一下:
生成SQL
通过上面MyBatis启动对Mapper.xml的解析,已经将xml转换为了java对象,并通过封装成了一个SqlSource对象,最后这个SqlSource对象跟着其它参数信息被封装成一个MappedStatement放入到Configuration中。
如我们可以通过session获取到Configuration对象,查看里面有的MappedStatement:
SysConfigMapper mapper = getSqlSession().getMapper(SysConfigMapper.class);
Collection<MappedStatement> mappedStatements = getSqlSession().getConfiguration().getMappedStatements();
我们可以看到,当SqlSource的实例为StaticSqlSource的时候,就可以直接查看到sql内容,这里是RawSqlSource 是纯文本,不存在动态节点。
而当SqlSource为DynamicSqlSource的时候,就无法看到一条完整的Sql语句了,而是由SqlNode实例组成的节点对象。
因此想要获取到Sql内容,还需要经历一步将动态SqlNode拼接成Sql语句的过程。
这里顺便提一个我遇到过的面试题:
MyBatis的Sql语句的生成是每次执行的时候动态生成,还是一次生成后重复利用?
答:MyBatis的Sql是每次动态生成的,因为每次执行sql的时候条件都可能不一样因此无法做到SQL语句重复利用,即使重复也会产生很多冗余SQL综合考虑不如每次执行动态拼接生成。
继续问,那么MyBatis的动态拼接sql是如何拼接的,是每次都要读取加载mapper.xml来生成吗?
答:不是,MyBatis在启动的时候,就已经将扫描到的mapper.xml文件生成了一个个的MappedStatement,存放到MyBatis上下文对象Configuration实例中了,并且存放的内容也是MyBatis内部定义的数据结构SqlNode,当mybatis启动后mapper.xml将没有任何关系,并在动态拼接sql的时候也是调用MyBatis内部对象进行拼接,而且不是每次都要创建对象,效率还不错。
接下来具体分析一下动态SqlNode拼接过程。
在前面又说到,SqlSource接口有个方法getBoundSql(Object parameterObject) 该方法会根据传入的parameterObject参数对象,拼接sql语句。
public class DynamicSqlSource implements SqlSource {
private final Configuration configuration; // 全局上下文对象
// sql片段的根节点,也就是前面标签解析调用MixedSqlNode rootSqlNode = parseDynamicTags(context);返回的值
private final SqlNode rootSqlNode;
public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
this.configuration = configuration;
this.rootSqlNode = rootSqlNode;
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
// 创建一个动态上下文
DynamicContext context = new DynamicContext(configuration, parameterObject);
// 从根节点开始,调用SqlNode的拼接sql方法,真正拼接预编译sql就在此处,每个SqlNode的apply方法就是拼接sql的逻辑
rootSqlNode.apply(context);
// 执行到这里,此时context中,就已经有了一条拼接完成的sql语句,但是还未做参数绑定,接下来会通过SqlSourceBuilder的parse方法
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
// 将sql中的#{}占位符给替换,替换成?,并生成对应的ParameterMapping,也就是参数和占位符关系
// 这里parse方法返回结果是,生成一个StaticSqlSource,因为sql拼接已经完成了,到这一步已经是一个完整sql语句了,所以使用StaticSqlSource
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
// 最后通过getBoundSql得到BoundSql对象,该BoundSql具有完整的sql语句以及,参数映射关系和条件
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}
}
Sql的生成要分两种情况讨论:
- 静态SqlSource,这种是解析完了xml中的sql节点后,是纯文本的sql节点,也就是说不包含任何动态标签如if、where等这种标签的sql节点。对于这种节点生成的SqlSource是RawSqlSource类型的,它不会涉及到sql拼接操作。
- 动态SqlSource,这种是解析完了xml中的sql节点后,带有像if、where这种动态标签的sql节点。这种节点生成的SqlSource是DynamicSqlSource。这种DynamicSqlSource类型要获取BoundSql,需要先通过SqlNode根据传来的参数动态拼接成一个静态sql语句。
总的来说SqlSource里面主要干了三件事:
1、将xml编写的sql节点,拼接解析为sql语句。
2、删除#{}占位符,替换成?生成预编译sql,并生成占位符和参数表达式的关系,返回SaticSqlSource。
3、根据传参生成BoundSql对象。
接下来针对DynamicSqlSource着重分析一下这是三个步骤:SqlNode动态拼接sql;sql参数表达式转换为预编译sql语句,创建StaticSqlSource;构建BoundSql对象。
SqlNode动态拼接sql
前面有说道,在解析mapper.xml后会生成以MixedSqlNode为根节点的一系列SqlNode实现类,SqlNode是一个接口,它有多个实现类,分别对应了DDM标签的动态标签,如IfSqlNode、ForEacheSqlNode、ChooseSqlNode...等。每个SqlNode都有个apply方法,该方法实现了动态标签的功能,以及拼接sql语句的功能。
如IfSqlNode类的功能,其它的SqlNode节点挺复杂的如ForEachSqlNode这种,需要自己去看,一时半会儿讲不清楚:
public class IfSqlNode implements SqlNode { private final ExpressionEvaluator evaluator; private final String test; private final 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 test="exp">里面的test表达式是否为true,如果为true则执行<if>标签里面节点 if (evaluator.evaluateBoolean(test, context.getBindings())) { contents.apply(context); return true; } return false; } }上面有说道,在xml中无论是如何写的sql语句,最后都会映射成一个个SqlNode的实现类,一般的静态文本都会生成TextSqlNode或者StaticSqlNode里面。
它们会调用,DynamicContext#appendSql来进行拼接sql:
public class StaticTextSqlNode implements SqlNode { private final String text; public StaticTextSqlNode(String text) { this.text = text; } @Override public boolean apply(DynamicContext context) { context.appendSql(text); return true; } }root节点为MixedSqlNode,采用深度优先遍历执行拼接。
public class MixedSqlNode implements SqlNode { private final List<SqlNode> contents; public MixedSqlNode(List<SqlNode> contents) { this.contents = contents; } @Override public boolean apply(DynamicContext context) { // 深度优先遍历 contents.forEach(node -> node.apply(context)); return true; } }
SqlNode动态拼接sql;sql参数表达式转换为预编译sql语句,创建StaticSqlSource
经过上面的步骤,已经完成了去除动态标签(where、if、foreach...),拼接成了一条仅带有#{}占位符的sql语句。
DynamicContext context = new DynamicContext(configuration, parameterObject); rootSqlNode.apply(context);
接下来,将要通过SqlSourceBuilder将sql转为预编译sql,即将#{}占位符转为对应的?,并且维护好参数绑定关系。
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration); Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass(); SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());主要是sqlSourceParser.parse()方法,进行解析转换,接下来看看里面的代码:
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) { // 创建一个#{param}表达式的控制器,这个控制器主要是为了处理#{param}表达式大括号里面的内容 ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters); // 创建表达式解析器,专门用来解析占位符#{},该解析器将会把sql字符串中的#{}字符解析出括号中间的内容,交给上面创建的handler来处理 GenericTokenParser parser = new GenericTokenParser("#{", "}", handler); String sql; // 调用parse执行解析,解析后的sql将不带有任何#{}字符串 if (configuration.isShrinkWhitespacesInSql()) {// 是否删除多余空格字符串,是mybatis全局配置的isShrinkWhitespacesInSql sql = parser.parse(removeExtraWhitespaces(originalSql)); } else { sql = parser.parse(originalSql); } // 当执行完上面的转换后,这条sql理所当然成了一条纯静态sql,不再有动态标签和占位符了 return new StaticSqlSource(configuration, sql, handler.getParameterMappings()); }
可能有人好奇,上面的解析只解析了#{}占位符,那么${}占位符是啥时候解析的呢?
在MyBatis中{}的解析,在第一步sql拼接就已经完成了,要不然你以为为啥拼接sql要让你传参数进去(
当然传参不只是为了这个节点功能,还有if判断等)。上面解释了从动态sql转为静态sql的整个过程,核心就在于字符串对#{}的解析,以及参数绑定。#{}解析是GenericTokenParser起作用,该类在多处有使用,如TextSqlNode对${}解析也用到了它,他就是一个字符串处理工具类,感兴趣的自己去看源码。
参数绑定:我们在写mybatis的sql的时候,我们可以对传来的参数名称多次使用,那么它们是如何绑定的呢?这个就是ParameterMappingTokenHandler所实现的功能了,最后我们看到返回的StaticSqlSource中第三个参数就是整个handler.getParameterMapping(),所以不难猜出,在ParameterMappingTokenHandler内部会进行参数维护,请看源码:
里面有个重要的类ParameterMapping,维护了sql的#{}占位符参数和预编译sql的?关系
public class ParameterMapping { private Configuration configuration; // 参数名称,也就是#{}括号里面的内容 private String property; // 参数模式 private ParameterMode mode; private Class<?> javaType = Object.class; // jdbc类型,如#{name,jdbcType=VARCHAR},则jdbcType对应了VARCHAR private JdbcType jdbcType; private Integer numericScale; private TypeHandler<?> typeHandler; private String resultMapId; private String jdbcTypeName; private String expression; }ParameterMappingTokenHandler#handleToken
// 参数绑定集合 private final List<ParameterMapping> parameterMappings = new ArrayList<>(); // 处理#{}括号里面的内容,这个content就是#{}里面的值,如#{name},则content值为name @Override public String handleToken(String content) { // 调用buildParameterMapping(content))生成ParameterMapping对象,ParameterMapping描述了当前占位符和参数之间关系 parameterMappings.add(buildParameterMapping(content)); // 将#{}括号里面的内容替换成? return "?"; }最后解析完,parameterMapping的索引就对应了预编译sql中的?,因为parameterMappings内部维护了ParameterMapping对象,因此在最后执行sql的时候就可以通过这个对象的属性值,从参数对象中取出相应的值,进行绑定执行。
如:select * from user where id=?; parameterMappings[{property:id....}]
到这里就完成sql转换和参数绑定映射关系维护
构建BoundSql对象
最后一步构建BoundSql,BoundSql就是一个存储解析完成的sql和对应参数相关信息的一个对象,没有复杂的逻辑:
public class BoundSql { // 预编译sql private final String sql; // 参数映射关系,通过上一步解析出来的 private final List<ParameterMapping> parameterMappings; // 调用mapper接口方法传来的参数 private final Object parameterObject; // 附加参数 private final Map<String, Object> additionalParameters; // 元数据 private final MetaObject metaParameters; }// 从上面第二步的解析已经知道了sqlSource类型为StaticSqlSource BoundSql boundSql = sqlSource.getBoundSql(parameterObject); context.getBindings().forEach(boundSql::setAdditionalParameter); // StaticSqlSource#getBoundSql,就是创建了一个BoundSql对象 @Override public BoundSql getBoundSql(Object parameterObject) { return new BoundSql(configuration, sql, parameterMappings, parameterObject); }
总结
MyBatis从mapper.xml解析到获取可执行sql的步骤:
总流程分析
- 第一步,通过LanguageDriver确定由什么语言去解析Sql语句,生成SqlSource
- 第二步,默认采用XMLLanguageDriver解析,在XMLLanguageDriver中将解析任务交给了XMLScriptBuilder来处理
- 第三步,XMLScriptBuilder中先是将xml中的sql片段所有信息转换成了对应SqlNode对象
- 第四步,在SqlSource中获取BoundSql,调用getBoundSql()方法的时候,将调用具体的SqlNode对象转换成sql语句,以及参数绑定,从而得到一个可以交由JDBC执行的sql语句