Mybatis sql解析过程
一、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对象的引用。
通过XMLStatementBuilder类的parseStatementNode方法解析xml
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.");
}
//如果还有动态的标签,递归调用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>
它会被解析成IfSqlNode
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的好处
————————————————
版权声明:本文为CSDN博主「bootstrap8」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u012387062/article/details/55005414
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· .NET Core 中如何实现缓存的预热?
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统