Mybatis源码解析:sql参数处理,原来可以这么简单-1

KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = context.getBooleanAttribute(“useGeneratedKeys”,
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
//解析Sql 根据sql文本来判断是否需要动态解析 如果没有动态sql语句且 只有#{}的时候 直接静态解析使用?占位 当有 ${} 不解析
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
StatementType statementType = StatementType.valueOf(context.getStringAttribute(“statementType”, StatementType.PREPARED.toString()));
//暗示驱动程序每次批量返回的结果行数
Integer fetchSize = context.getIntAttribute(“fetchSize”);
//超时时间
Integer timeout = context.getIntAttribute(“timeout”);
//引用外部 parameterMap,已废弃
String parameterMap = context.getStringAttribute(“parameterMap”);
//结果类型
String resultType = context.getStringAttribute(“resultType”);
Class<?> resultTypeClass = resolveClass(resultType);
//引用外部的 resultMap
String resultMap = context.getStringAttribute(“resultMap”);
//结果集类型,FORWARD_ONLY|SCROLL_SENSITIVE|SCROLL_INSENSITIVE 中的一种
String resultSetType = context.getStringAttribute(“resultSetType”);
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
if (resultSetTypeEnum == null) {
resultSetTypeEnum = configuration.getDefaultResultSetType();
}
//(仅对 insert 有用) 标记一个属性, MyBatis 会通过 getGeneratedKeys 或者通过 insert 语句的 selectKey 子元素设置它的值
String keyProperty = context.getStringAttribute(“keyProperty”);
String keyColumn = context.getStringAttribute(“keyColumn”);
String resultSets = context.getStringAttribute(“resultSets”);

builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

找到解析sql的部分具体来分析,一层一层往下。

SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
RawLanguageDriver.createSqlSource 该类是XMLLanguageDriver的子类

@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
SqlSource source = super.createSqlSource(configuration, script, parameterType);
checkIsNotDynamic(source);
return source;
}
XMLLanguageDriver.createSqlSource

public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
}
XMLScriptBuilder.parseScriptNode

public SqlSource parseScriptNode() {
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
//判断节点是否是动态的,包含是否包含if、where 、choose、trim、foreach、bind、sql标签,这个例子中我们进入else
if (isDynamic) {
//不解析
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
//用占位符方式来解析
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
这里进行判断isDynamic的值,这个方法我们只需要关注textSqlNode.isDynamic()就行了。代码与之前解析node有些类似。

protected MixedSqlNode parseDynamicTags(XNode node) {
List contents = new ArrayList<>();
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
//注意!!这里又new了一个XNode,也就是说,这个节点中的sql语句又被解析了一次,解析方式和上文从同全局获取变量一样。
//与上文不同的是,这里传入的是子节点,也就是sql文本语句,而上文解析的是整个select元素
//这个child是临时变量,节点解析的结果不做保存
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 = 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 boolean isDynamic() {
DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
//这里创建一个解析器进行解析sql语句,这里解析的是仍然是KaTeX parse error: Expected 'EOF', got '}' at position 103: …r.isDynamic(); }̲ private Generi…{", “}”, handler);
}
熟悉的代码,还是同样的解析器,用来处理${,和},不过这次的hander不同,为DynamicCheckerTokenParser

//DynamicCheckerTokenParser.handleToken
public String handleToken(String content) {
this.isDynamic = true;
return null;
}
}
这次的处理方式是将直接返回空,也就是说,sql会变成 select * from user where id = null。但是返回的结果并没有被保存,parser.parse(text)并没有参数来接受它的返回值,所以这里只是用来更新isDynamic参数。

回到XMLScriptBuilder.parseScriptNode方法,这里根据isDynamic的布尔值,会有两种SqlSource.DynamicSqlSource和RawSqlSource。到这里配置文件就解析完成了,后续sql中的参数都是从方法中获取的,所以只能在执行的时候动态进行替换。

来到query查询方法,方法在第三章执行sql的时候简单说过。ms.getBoundSql会获取绑定的封装sql.

//CachingExecutor.query
public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
MappedStatement.getBoundSql

public BoundSql getBoundSql(Object parameterObject) {
//获取绑定的sql
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
//获取sql中对应的参数
List 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;
}
//DynamicSqlSource.getBoundSql。
public BoundSql getBoundSql(Object parameterObject) {
//parameterObject中有我们方法传入的参数
DynamicContext context = new DynamicContext(configuration, parameterObject);
//这里解析KaTeX parse error: Expected 'EOF', got '}' at position 441: …turn boundSql; }̲ 为什么是DynamicSql…{},使用的就是DynamicSqlSource。

//MixedSqlNode.apply
public boolean apply(DynamicContext context) {
contents.forEach(node -> node.apply(context));
return true;
}
TextSqlNode.apply

@Override
public boolean apply(DynamicContext context) {
contents.forEach(node -> node.apply(context));
return true;
}
这里再次创建了${}的解析器,这次的handler是BindingTokenParser

public boolean apply(DynamicContext context) {
GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
context.appendSql(parser.parse(text));
return true;
}

private GenericTokenParser createParser(TokenHandler handler) {
return new GenericTokenParser("KaTeX parse error: Expected 'EOF', got '}' at position 20: …"}", handler); }̲ BindingTokenPa…{},就会将其替换成具体的参数,语句就变成 select * from user where id = 1,就能直接执行了

public String handleToken(String content) {
Object parameter = context.getBindings().get(“_parameter”);
if (parameter == null) {
context.getBindings().put(“value”, null);
} else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
context.getBindings().put(“value”, parameter);
}
Object value = OgnlCache.getValue(content, context.getBindings());
String srtValue = value == null ? “” : String.valueOf(value); // issue #274 return “” instead of “null”
checkInjection(srtValue);
return srtValue;
}
入参#{}的解析
那么如果是#{}该怎么处理呢?

select * from user where id = #{id} List get(Integer id); 由上文得知,由于没有${},那么SqlSource就会变成RawSqlSource。在创建RawSqlSource的时候,在构造方法中就会对#{}解析。

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;
sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
}
SqlSourceBuilder.parse

public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
GenericTokenParser parser = new GenericTokenParser(“#{”, “}”, handler);
String sql = parser.parse(originalSql);
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
这里用的hander是ParameterMappingTokenHandler,它的作用是将#{XXX}替换成 ?

ParameterMappingTokenHandler.handleToken

public String handleToken(String content) {
parameterMappings.add(buildParameterMapping(content));
return “?”;
}
这时sql就变成了select * from user where id = ?,到这里还只是解析配置文件。在具体执行方法时也要调用getBoundSql方法将参数进行赋值

//RawSqlSource.getBoundSql
public BoundSql getBoundSql(Object parameterObject) {
return sqlSource.getBoundSql(parameterObject);
}
StaticSqlSource.getBoundSql,最后调用BoundSql的构造方法,将sql语句,入参等传入

public BoundSql getBoundSql(Object parameterObject) {
return new BoundSql(configuration, sql, parameterMappings, parameterObject);
}
之后就要创建数据库连接,进行查询了。回到这个方法SimpleExecutor.prepareStatement。回顾一下,这是创建StatementHandler后做的一些连接数据库的准备操作。

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
//获取jdbc数据库连接
Connection connection = getConnection(statementLog);
//一些准备工作,初始化Statement连接
stmt = handler.prepare(connection, transaction.getTimeout());
//使用ParameterHandler处理入参
handler.parameterize(stmt);
return stmt;
}
我们先进入这个方法PreparedStatementHandler.parameterize。

为什么是PreparedStatementHandler之前也说过,因为语句的默认类型是PREPARED, 还有其他的类型如果是CALLABLE,对应CallableStatementHandler,STATEMENT对应SimpleStatementHandler。可以用参数statementType进行设置。

@Override
public void parameterize(Statement statement) throws SQLException {
parameterHandler.setParameters((PreparedStatement) statement);
}
DefaultParameterHandler.setParameters.

@Override
public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity(“setting parameters”).object(mappedStatement.getParameterMap().getId());
//boundSql用来解析我们的sql语句,parameterMappings是我们传入的参数
List parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
for (int i = 0; i < parameterMappings.size(); i++) {
//这里第一个参数就是id
ParameterMapping parameterMapping = parameterMappings.get(i);
//mode属性允许能指定IN,OUT或INOUT参数。如果参数的 mode 为 OUT 或 INOUT,将会修改参数对象的属性值,以便作为输出参数返回。
//#{id}默认mode为OUT
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
//这里是boundsql中的额外参数,可以使用拦截器添加,例子放在下文
if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
//如果类型处理器中有这个类型,那么直接赋值就行了,例如这里是Integer类型,类型处理器是有的
//那么直接赋值
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
//如果不是的会转化为元数据进行处理,metaObject元数据可以理解为用来反射的工具类,可以处理参数的get,set
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 | SQLException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
}
}
}
}
}
value如果是空的那么就直接设置为jdbc的空类型,不为空调用具体的类型处理器。

BaseTypeHandler.setParameter。该类是所有typeHandler的父类.如果不为空调用setNonNullParameter,该方法时抽象的,由具体的子类实现。这里使用的是一个相当于路由的的子类UnknownTypeHandler,这个子类可以根据传入的类型,再去找到具体的类型处理器,例如IntegerTypeHander.

@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);
    }
    }
    }
    UnknownTypeHandler.setNonNullParameter

public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType)
throws SQLException {
TypeHandler handler = resolveTypeHandler(parameter, jdbcType);
handler.setParameter(ps, i, parameter, jdbcType);
}
UnknownTypeHandler.resolveTypeHandler这个方法根据传入的参数类型,找到具体的TypeHandler

private TypeHandler<?> resolveTypeHandler(Object parameter, JdbcType jdbcType) { TypeHandler<?> handler;
if (parameter == null) {
handler = OBJECT_TYPE_HANDLER;
} else {
handler = typeHandlerRegistry.getTypeHandler(parameter.getClass(), jdbcType);
// check if handler is null (issue #270)
if (handler == null || handler instanceof UnknownTypeHandler) {
handler = OBJECT_TYPE_HANDLER;
}
}
return handler;
}
例如如果这个参数是id,Integer类型,那么就会找到IntegerTypeHandler

//IntegerTypeHandler
public void setNonNullParameter(PreparedStatement ps, int i, Integer parameter, JdbcType jdbcType)
throws SQLException {
ps.setInt(i, parameter);
}
最后还是使用jdbc的PreparedStatement处理参数。

附:自定义的拦截器用来加入参数。

@Intercepts({
@Signature(type = StatementHandler.class, method = “prepare”, args = {Connection.class,Integer.class})
})
public class MyInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler bs = (StatementHandler) invocation.getTarget();
BoundSql boundSql = bs.getBoundSql();
boundSql.setAdditionalParameter(“id”,“1”);
return invocation.proceed();
}
}
例4 ${}和#{}都存在的情况
如果是都存在的情况呢?

select * from user where id = ${id} AND name = #{name} List findUserByIdAndName(@Param("id") Integer id, @Param("name") String name); 结合上文的分析,由于存在${},所以选择的DynamicSqlSource。

DynamicSqlSource.getBoundSql。这个方法上文分析到了rootSqlNode.apply(context);会将${}替换成具体参数。我们接着分析。

public BoundSql getBoundSql(Object parameterObject) {
//parameterObject中有我们方法传入的参数
DynamicContext context = new DynamicContext(configuration, parameterObject);
//解析${}并替换成具体的值
rootSqlNode.apply(context);
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
//这里又进行了一次解析
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}
解析#{},并将其替换成?

//RawSqlSource.parse
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
GenericTokenParser parser = new GenericTokenParser(“#{”, “}”, handler);
String sql = parser.parse(originalSql);
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
这样我们的语句就变成了select * from user where id = 1 AND name = ?,然后调用sqlSource.getBoundSql

//StaticSqlSource.getBoundSql

总结

总体来说,如果你想转行从事程序员的工作,Java开发一定可以作为你的第一选择。但是不管你选择什么编程语言,提升自己的硬件实力才是拿高薪的唯一手段。

如果你以这份学习路线来学习,你会有一个比较系统化的知识网络,也不至于把知识学习得很零散。我个人是完全不建议刚开始就看《Java编程思想》、《Java核心技术》这些书籍,看完你肯定会放弃学习。建议可以看一些视频来学习,当自己能上手再买这些书看又是非常有收获的事了。

ource(configuration, sql, handler.getParameterMappings());
}
这样我们的语句就变成了select * from user where id = 1 AND name = ?,然后调用sqlSource.getBoundSql

//StaticSqlSource.getBoundSql

总结

总体来说,如果你想转行从事程序员的工作,Java开发一定可以作为你的第一选择。但是不管你选择什么编程语言,提升自己的硬件实力才是拿高薪的唯一手段。

如果你以这份学习路线来学习,你会有一个比较系统化的知识网络,也不至于把知识学习得很零散。我个人是完全不建议刚开始就看《Java编程思想》、《Java核心技术》这些书籍,看完你肯定会放弃学习。建议可以看一些视频来学习,当自己能上手再买这些书看又是非常有收获的事了。

[外链图片转存中…(img-fwqfF3NE-1714421316051)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

  • 9
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值