首先我们来说下预编译的一个背景:我们知道一条SQL
语句到达Mysql
之后,Mysql
并不是会马上执行它,而是需要经过几个阶段性的动作(细节的可以查看Mysql复习计划(一)- 字符集、文件系统和SQL执行流程):
那么这几个阶段肯定是需要一定的时间的。而有时候我们一条SQL
语句可能需要反复的执行,只不过里面的参数可能不一样,比如where
子句中的条件。如果每次都需要经过上面的几个步骤,那么效率就会下降。因此为了解决这种问题,就出现了预编译。
预编译语句就是将这类语句中的值用占位符替代,可以视为将SQL
语句模板化或者说参数化。一次编译、多次运行,省去了解析优化等过程。
Mybatis
中,默认是开启预编译的功能的,我们来测试一下(用的SpringBoot
):
1.pom
依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.1</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>
2.配置文件:application.yml
文件。
server:
port: 8080
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://IP地址:3306/数据库名称
username: xxx
password: xxx
# 别名,这样写Mapper配置文件的时候可以省略前缀
mybatis:
type-aliases-package: com.application.bean
application.properties
文件:
#druid数据库连接池
type=com.alibaba.druid.pool.DruidDataSource
#配置mapper
mybatis.mapper-locations=classpath:mapper/*.xml
#配置日志输出
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--关联UserMapper接口 ,下面的id要和接口中对应的方法名称一致,实现一对一的对应-->
<mapper namespace="com.application.mapper.UserMapper">
<select id="getUserById" parameterType="String" resultType="user">
select * from user where userName=#{userName}
</select>
</mapper>
public class User {
private String userName;
private int id;
// get/set
}
5.mapper
接口:
@Mapper
@Repository
public interface UserMapper {
User getUserById(String userName);
}
6.Service
类:
@Service
public class UserProcess {
@Autowired
private UserMapper userMapper;
public User getUser(String name){
return userMapper.getUserById(name);
}
}
7.Controller
类:
@RestController
public class MyController {
@Autowired
private UserProcess userProcess;
@PostMapping("/hello")
public User hello(){
return userProcess.getUser("tom");
}
}
8.运行后的日志输出:
当你看到了占位符 ?
的时候,就说明预编译成功了,Mybatis
就是将#{}
这样的参数用占位符问号来代替。形成一个SQL
模板。以达到预编译的目的。
Mybatis
中,对于SQL
的处理解析动作在于XMLStatementBuilder.parseStatementNode
这个类的函数中,我们来从这里为入口来看。我们先来看本文案例中的Mapper
文件:
<mapper namespace="com.application.mapper.UserMapper">
<select id="getUserById" parameterType="String" resultType="user">
select * from user where userName=#{userName}
</select>
</mapper>
再看下源码:
public void parseStatementNode() {
// 命名空间中的唯一标识,一般可以用方法名
String id = context.getStringAttribute("id");
// 对应的数据库标识
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
// 驱动程序每次批量返回的结果行数
Integer fetchSize = context.getIntAttribute("fetchSize");
// 等待数据库返回结果的超时时间
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
// 该SQL中,传入的参数的完全限定名或者别名。本文是user
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
// Map结果集
String resultMap = context.getStringAttribute("resultMap");
// 该SQL返回类型
String resultType = context.getStringAttribute("resultType");
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
Class<?> resultTypeClass = resolveClass(resultType);
// 结果集的相关配置,下文给出详细的点
String resultSetType = context.getStringAttribute("resultSetType");
// 语句类型的处理,下文给出详细的点
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
String nodeName = context.getNode().getNodeName();
// 得到SQL命令的类型:对应着增删改查,有这么几种枚举值:UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH;
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
// 设置为true的时候,只要语句被调用,就会让本地缓存和二级缓存被清空。默认值为false(针对select)
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
// true代表将本次查询的语句通过二级缓存缓存起来,针对select,为true。
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// Include Fragments before parsing
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// Parse selectKey after includes and remove them.
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// 生成SQL的地方。
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
// ...省略
}
resultSetType
有哪些类型?
FORWARD_ONLY
:结果集的游标只能向下滚动。SCROLL_INSENSITIVE
:结果集的游标可以上下移动,当数据库变化时,当前结果集不变。SCROLL_SENSITIVE
:返回可滚动的结果集,当数据库变化时,当前结果集同步改变。statementType
有哪些类型?
STATEMENT
:普通语句。PREPARED
:预处理。CALLABLE
:存储过程。那么本文只关注于预编译的处理过程。我们继续看上述函数中的关键代码:
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
这段代码就是通过LanguageDriver
对SQL
语句进行解析,返回的结果中,就可能包含动态SQL
或者占位符。我们来看下这个函数的源码:LanguageDriver
的默认实现是XMLLanguageDriver
。
public class XMLLanguageDriver implements LanguageDriver {
// 接着到这里
@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
// 创建一个XMLScriptBuilder对象。并通过它来解析SQL脚本
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
}
↓↓↓↓↓↓↓
public SqlSource parseScriptNode() {
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource = null;
// 这里就是重点了,判断这个SQL是否是动态的
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
}
从上面源码可以得出,最终SQL
构建起来,有两个分支:
DynamicSqlSource
来构建。RawSqlSource
来构建。那么是否为动态的判断标准是啥呢?关键看下面这行代码:
MixedSqlNode rootSqlNode = parseDynamicTags(context);
↓↓↓↓↓↓↓
protected MixedSqlNode 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 ,然后判断是否是动态的
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 handler = nodeHandlerMap.get(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
handler.handleNode(child, contents);
isDynamic = true;
}
}
return new MixedSqlNode(contents);
}
↓↓↓↓↓↓↓
textSqlNode.isDynamic()
↓↓↓↓↓↓↓
public class TextSqlNode implements SqlNode {
public boolean isDynamic() {
DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
GenericTokenParser parser = createParser(checker);
parser.parse(text);
return checker.isDynamic();
}
↓↓↓↓↓↓↓
private GenericTokenParser createParser(TokenHandler handler) {
return new GenericTokenParser("${", "}", handler);
}
}
到这里为止,我们可以观察到,在判断这个SQL
是否为动态的时候,底层会创建一个GenericTokenParser
类型的解析器。而这个解析器呢则是一个以 ${
为开始和以 }
为结尾的解析器。如果解析成功,说明该SQL
中包含了${}
,那么该SQL
就会被标记为动态SQL
标签。
那么反之,我们回到这段代码中:
对于#{}
这样的语句,就是一个非动态标签。总结下就是:
SQL
中的参数是用${}
作为占位符的,那么该SQL
属于动态SQL
,封装为DynamicSqlSource
。SQL
,封装为RawSqlSource
。那么我们来看下RawSqlSource
的构造函数:
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>());
}
public class SqlSourceBuilder extends BaseBuilder {
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
// 主要的一个解析器
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
// 获取真实的可执行性的sql语句
String sql = parser.parse(originalSql);
// 包装成StaticSqlSource然后返回
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
}
这里我们可以看出来,对于非动态SQL
,会生成一个以 #{
为开头,}
为结尾的解析器。紧接着就会创建一个StaticSqlSource
类,在这里做个区分:
RawSqlSource
: 存储的是只有 #{}
或者没有标签的纯文本SQL
信息DynamicSqlSource
: 存储的是写有 ${}
或者具有动态SQL
标签的SQL
信息StaticSqlSource
: 是DynamicSqlSource
和RawSqlSource
解析为BoundSql
的一个中间态对象类型。BoundSql
:用于生成我们最终执行的SQL
语句,属性包括参数值、映射关系、以及SQL
(带问号的)接着我们看下解析器ParameterMappingTokenHandler
的处理,它是SqlSourceBuilder
的一个静态内部类:
private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {
@Override
public String handleToken(String content) {
parameterMappings.add(buildParameterMapping(content));
return "?";
}
}
这段代码在哪里用到呢?就是在GenericTokenParser.parse()
这段代码中执行到的:
public class GenericTokenParser {
public String parse(String text) {
// ...
while (start > -1) {
if (start > 0 && src[start - 1] == '\\') {
// this open token is escaped. remove the backslash and continue.
builder.append(src, offset, start - offset - 1).append(openToken);
offset = start + openToken.length();
} else {
// ...
if (end == -1) {
// close token was not found.
builder.append(src, start, src.length - start);
offset = src.length;
} else {
// 就是找到了#{}的结束标识},然后将中间的内容替换成?
builder.append(handler.handleToken(expression.toString()));
offset = end + closeToken.length();
}
}
start = text.indexOf(openToken, offset);
}
if (offset < src.length) {
builder.append(src, offset, src.length - offset);
}
return builder.toString();
}
}
这段代码执行完毕之后,我们发现最后返回了一个StaticSqlSource
对象。
那么在执行SQL
的时候,则会去根据BoundSql
来完成参数的赋值等操作。我们来看下RawSqlSource.getBoundSql
这个函数:
public class RawSqlSource implements SqlSource {
private final SqlSource sqlSource;
@Override
public BoundSql getBoundSql(Object parameterObject) {
return sqlSource.getBoundSql(parameterObject);
}
}
因为只有#{}
的SQL
语句,在上文中可以看到最后会生成一个StaticSqlSource
对象,而这个类中就重写了getBoundSql
函数,里面主要构造了一个BoundSql
对象。
public class StaticSqlSource implements SqlSource {
@Override
public BoundSql getBoundSql(Object parameterObject) {
return new BoundSql(configuration, sql, parameterMappings, parameterObject);
}
}
Mybatis
在执行SQL
的时候,主要看的是SimpleExecutor.prepareStatement
这个入口:
public class SimpleExecutor extends BaseExecutor {
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
// 获取JDBK的数据库连接
Connection connection = getConnection(statementLog);
// 准备工作,初始化Statement连接
stmt = handler.prepare(connection, transaction.getTimeout());
// 使用ParameterHandler处理入参
handler.parameterize(stmt);
return stmt;
}
}
我们主要关注入参的处理函数上:
public class PreparedStatementHandler extends BaseStatementHandler {
@Override
public void parameterize(Statement statement) throws SQLException {
parameterHandler.setParameters((PreparedStatement) statement);
}
}
↓↓↓↓↓
public class DefaultParameterHandler implements ParameterHandler {
@Override
public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
// 从boundSql中拿到我们传入的参数
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
// 循环处理每一个参数,需要应用到类型转换器
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
// mode属性有三种:IN, OUT, INOUT。如果参数为 OUT 或 INOUT,参数对象属性的真实值将会被改变
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
}
// 如果类型处理器里面有这个类型,直接赋值即可。
else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
}
// 否则转化为元数据处理,通过反射来完成get/set赋值
else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
try {
// 使用不同的类型处理器向jdbc中的PreparedStatement设置参数
typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
} catch (SQLException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
}
}
}
}
}
}
前面一部分逻辑主要是对参数进行类型转换。最后则对SQL
进行参数的赋值和替换。那么我们主要关注最后的赋值代码:
typeHandler.setParameter(ps, i + 1, value, jdbcType);
↓↓↓↓↓
public abstract class BaseTypeHandler<T> extends TypeReference<T> implements TypeHandler<T> {
@Override
public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
if (parameter == null) {
if (jdbcType == null) {
throw new TypeException("JDBC requires that the JdbcType must be specified for all nullable parameters.");
}
try {
ps.setNull(i, jdbcType.TYPE_CODE);
} catch (SQLException e) {
throw new TypeException("Error setting null for parameter #" + i + " with JdbcType " + jdbcType + " . " +
"Try setting a different JdbcType for this parameter or a different jdbcTypeForNull configuration property. " +
"Cause: " + e, e);
}
} else {
try {
setNonNullParameter(ps, i, parameter, jdbcType);
} catch (Exception e) {
throw new TypeException("Error setting non null for parameter #" + i + " with JdbcType " + jdbcType + " . " +
"Try setting a different JdbcType for this parameter or a different configuration property. " +
"Cause: " + e, e);
}
}
}
}
我们重点关注那些非null
值的赋值,看下Mybatis
是怎么替换SQL
中的 ?
的:
setNonNullParameter(ps, i, parameter, jdbcType);
这个函数是一个抽象函数,有很多具体的实现,对应的参数是什么类型的,就用对应类型的处理方式去完成,如图:
无论是哪种类型,本质上都是对 ?
占位符进行值的替换操作。
因为本篇文章主要是将Mybatis
对于预处理的原理。因此整个Mybatis
的机制或者是二级缓存、一级缓存的知识点不在本文内容范围中(准备再写几篇内容补上)。本文主要讲了:什么是预编译。Mybatis
中对预编译的处理、如何生成模板SQL
、如何进行值的替换。
因此这里做个小总结:
首先预编译对于Mybatis
而言,相当于构建出了一条SQL
的模板,将#{}
对应的参数改为?
而已。届时只需要更改参数的值即可,无需再对SQL
进行语法解析等操作。
对于动态SQL
的判断,就是在于是否包含${}
占位符。如果包含了就通过DynamicSqlSource
来解析。而这里则影响到SQL
的解析:
DynamicSqlSource
:解析包含${}
的语句,其实也会解析#{}
的语句。RawSqlSource
:解析只包含#{}
的语句。这两种类型到最后都会转化为StaticSqlSource
,然后由他创建一个BoundSql
对象。包括参数值、映射关系、以及转化好的SQL
。
最后是关于SQL
的执行,即如何将真实的参数赋值到我们上面生成的模板SQL
中。这部分逻辑发生在SQL
的执行过程中,其入口SimpleExecutor.prepareStatement
主要做了这么几件事。
BoundSql
对象。拿到我们传入的参数。null
,则直接赋值,否则,还要通过类型处理器来完成赋值操作。typeHandler.setParameter(ps, i + 1, value, jdbcType);
备注,Mybatis支持的类型处理器可以看TypeHandlerRegistry这个类,相关处理器的注册在其构造函数中。这里贴出部分截图: