sql预编译
定义
sql 预编译指的是数据库驱动在发送 sql 语句和参数给 DBMS 以前对 sql 语句进行编译,这样 DBMS 执行 sql 时,就不须要从新编译。
为何须要预编译
JDBC 中使用对象 PreparedStatement 来抽象预编译语句,使用预编译
- 预编译阶段能够优化 sql 的执行。
预编译以后的 sql 多数状况下能够直接执行,DBMS 不须要再次编译,越复杂的sql,编译的复杂度将越大,预编译阶段能够合并屡次操做为一个操做。 - 预编译语句对象能够重复利用。
把一个 sql 预编译后产生的 PreparedStatement 对象缓存下来,下次对于同一个sql,能够直接使用这个缓存的 PreparedState 对象。
mybatis 默认状况下,将对全部的 sql 进行预编译。
mysql预编译源码解析
mysql 的预编译源码在 com.mysql.jdbc.ConnectionImpl 类中,以下:
public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
synchronized(this.getConnectionMutex()) {
this.checkClosed();
com.mysql.jdbc.PreparedStatement pStmt = null;
boolean canServerPrepare = true;
String nativeSql = this.getProcessEscapeCodesForPrepStmts() ? this.nativeSQL(sql) : sql;
if (this.useServerPreparedStmts && this.getEmulateUnsupportedPstmts()) {
canServerPrepare = this.canHandleAsServerPreparedStatement(nativeSql);
}
if (this.useServerPreparedStmts && canServerPrepare) {
if (this.getCachePreparedStatements()) {
synchronized(this.serverSideStatementCache) {
pStmt = (com.mysql.jdbc.PreparedStatement)this.serverSideStatementCache.remove(new ConnectionImpl.CompoundCacheKey(this.database, sql));
if (pStmt != null) {
((ServerPreparedStatement)pStmt).setClosed(false);
((com.mysql.jdbc.PreparedStatement)pStmt).clearParameters();
}
if (pStmt == null) {
try {
pStmt = ServerPreparedStatement.getInstance(this.getMultiHostSafeProxy(), nativeSql, this.database, resultSetType, resultSetConcurrency);
if (sql.length() < this.getPreparedStatementCacheSqlLimit()) {
((ServerPreparedStatement)pStmt).isCached = true;
}
((com.mysql.jdbc.PreparedStatement)pStmt).setResultSetType(resultSetType);
((com.mysql.jdbc.PreparedStatement)pStmt).setResultSetConcurrency(resultSetConcurrency);
} catch (SQLException var13) {
if (!this.getEmulateUnsupportedPstmts()) {
throw var13;
}
pStmt = (com.mysql.jdbc.PreparedStatement)this.clientPrepareStatement(nativeSql, resultSetType, resultSetConcurrency, false);
if (sql.length() < this.getPreparedStatementCacheSqlLimit()) {
this.serverSideStatementCheckCache.put(sql, Boolean.FALSE);
}
}
}
}
} else {
try {
pStmt = ServerPreparedStatement.getInstance(this.getMultiHostSafeProxy(), nativeSql, this.database, resultSetType, resultSetConcurrency);
((com.mysql.jdbc.PreparedStatement)pStmt).setResultSetType(resultSetType);
((com.mysql.jdbc.PreparedStatement)pStmt).setResultSetConcurrency(resultSetConcurrency);
} catch (SQLException var12) {
if (!this.getEmulateUnsupportedPstmts()) {
throw var12;
}
pStmt = (com.mysql.jdbc.PreparedStatement)this.clientPrepareStatement(nativeSql, resultSetType, resultSetConcurrency, false);
}
}
} else {
pStmt = (com.mysql.jdbc.PreparedStatement)this.clientPrepareStatement(nativeSql, resultSetType, resultSetConcurrency, false);
}
return (PreparedStatement)pStmt;
}
}
流程图以下所示:
mybatis之sql动态解析以及预编译源码
mybatis sql 动态解析
mybatis 在调用 connection 进行 sql 预编译以前,会对sql语句进行动态解析,动态解析主要包含以下的功能:
- 占位符的处理
- 动态sql的处理
- 参数类型校验
mybatis强大的动态SQL功能的具体实现就在此。
{}和${}
在上一篇文章我是解析到获取BoundSql,并且最后我是发出了一张BoundSql的结构 image.png
方法:
Peopel selectOne(@Param("name")String name, @Param("id")int id);
配置文件:
<select id='selectOne'>
select * from test where 1 = 1 and id = ${id} and name = #{name}
</select>
复制代码
调用的方法与配置的sql语句。 在之前我们知道我们在配置文件写的sql语句最后是被解析成一个个的SqlNode,然后将一个个的SqlNode存储到SqlSourse对象中去。不清楚的可以看下我之前的文章。在我主页里面有关于解析MyBatis源码的文章。废话不多说了。继续上次的文章,juejin.cn/post/684490… 截屏2019-11-10上午11.36.28
这个DynamicContext是构建完整Sql语句的上下文。 this.rootSqlNode.apply(context);是将一个个的SqlNode拼接起来,底层也就是用了StringBuilder.append()方法拼接的。关于${}的解析也是在这个方法实现的。在包org.apache.ibatis.scripting.xmltags.TextSqlNode下有这样的一个方法。如下:
就是在解析我们的sql语句的时候如果碰到{}位置,然后进行字符串替换。所以说
{}符号中的值,还记得之前我们在解析的时候方法参数的解析吗??方法参数会被解析好然后放到一个map中。所以此时我们得到${}中的值就是map中的key,通过这个key去拿到值,然后进行字符串替换即可。
解析完${}符号,接下来就是#{}了。
我们进入到这个parse()方法中
跟解析${}一样,但是#{}不是字符串替换,而是用 ? 代替。 我们知道方法参数 public void selectById(@Params("name")String name,@Params("id")int id) 会被ParamNameResolver解析成一个Map,Map中KEY值自然就是注解@Param中的值,VALUE就是我们传进来的值,在解析#{}占位符的时候,每一个#{}占位符都会被解析成一个ParameterMapping对象,ParameterMapping{property='name',mode=IN,javaType=class java.lang.String,jdbcType=null....},当然id也会解析成这个格式,然后将解析好的ParameterMapping存放到一个List中去,具体实现是在 ParameterMappingTokenHandle类中的handleToken方法实现的。我们知道List是个有序集合,所以在将❓替换成我们传的参数就会遍历这个list,然后与我们的参数一一对应起来。整个流程如下图 image.png
循环list得到每个ParameterMapping对象,然后得到该对象的property属性,最后从参数Map中获取到值,之前说过参数经过解析会存储在一个Map中的,在经过这一步之后就能得到一条完整的SQL语句了,select * from test where name = #{name} and id = #{id} 就会解析成 select * from test where name = "bjh" and id = 1。
所以在编写sql配置文件的时候如果使用${},那么如果是字符串类型要加上 '',不然会报错,尽量使用#{},防止sql注入。整个的解析流程就是这样,得到了完整的sql语句那么就执行就可以了。执行下次在写吧。
总结
值可以使用#{}替换,但如果是mysql表或者字段关键字不能使用 #{} (1)、mybatis能使用#{}的时候尽量使用#{} (2)、表名、order by的排序字段作为变量时,使用{}。 (3)、#不需要关注数据类型,mybatis实现自动数据类型转换;不做数据类型转换,需要自行判断数据类型 (4)、#是预编译的方式,$是直接拼接;