一、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