mybatis深入理解(一)之 # 与 $ 区别以及 sql 预编译
mybatis 中使用 sqlMap 進行 sql 查詢時,經常需要動態傳遞參數,例如我們需要根據用戶的姓名來篩選用戶時,sql 如下:
select?*?from?user?where?name =?"ruhua";上述 sql 中,我們希望 name 后的參數 "ruhua" 是動態可變的,即不同的時刻根據不同的姓名來查詢用戶。在 sqlMap 的 xml 文件中使用如下的 sql 可以實現動態傳遞參數 name:
select * from user where name = #{name};或者
select * from user where name = ${name};對于上述這種查詢情況來說,使用 #{ } 和 ${ } 的結果是相同的,但是在某些情況下,我們只能使用二者其一。
'#' 與 '$'
區別
動態 SQL?是 mybatis 的強大特性之一,也是它優于其他 ORM 框架的一個重要原因。mybatis 在對 sql 語句進行預編譯之前,會對 sql 進行動態解析,解析為一個 BoundSql 對象,也是在此處對動態 SQL 進行處理的。
在動態 SQL 解析階段, #{ } 和 ${ } 會有不同的表現:
#{ } 解析為一個 JDBC 預編譯語句(prepared statement)的參數標記符。
例如,sqlMap 中如下的 sql 語句
select?*?from?user?where?name = #{name};解析為:
select?*?from?user?where?name = ?;一個 #{ } 被解析為一個參數占位符???。
而,
${ } 僅僅為一個純碎的 string 替換,在動態 SQL 解析階段將會進行變量替換
例如,sqlMap 中如下的 sql
select?*?from?user?where?name = ${name};當我們傳遞的參數為 "ruhua" 時,上述 sql 的解析為:
select?*?from?user?where?name =?"ruhua";預編譯之前的 SQL 語句已經不包含變量 name 了。
綜上所得, ${ } 的變量的替換階段是在動態 SQL 解析階段,而 #{ }的變量的替換是在 DBMS 中。
用法 tips
1、能使用 #{ } 的地方就用 #{ }
首先這是為了性能考慮的,相同的預編譯 sql 可以重復利用。
其次,${ } 在預編譯之前已經被變量替換了,這會存在 sql 注入問題。例如,如下的 sql,
select?*?from?${tableName}?where?name =?#{name}假如,我們的參數 tableName 為?user; delete user; --,那么 SQL 動態解析階段之后,預編譯之前的 sql 將變為
select?*?from?user;?delete?user;?-- where name = ?;--?之后的語句將作為注釋,不起作用,因此本來的一條查詢語句偷偷的包含了一個刪除表數據的 SQL!
2、表名作為變量時,必須使用 ${ }
這是因為,表名是字符串,使用 sql 占位符替換字符串時會帶上單引號?'',這會導致 sql 語法錯誤,例如:
select?*?from?#{tableName}?where?name = #{name};預編譯之后的sql 變為:
select?*?from???where?name = ?;假設我們傳入的參數為 tableName = "user" , name = "ruhua",那么在占位符進行變量替換后,sql 語句變為
select?*?from?'user'?where?name='ruhua';上述 sql 語句是存在語法錯誤的,表名不能加單引號?''(注意,反引號 ``是可以的)。
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 synchronized java.sql.PreparedStatement prepareStatement(String sql,int resultSetType, int resultSetConcurrency) throws SQLException {checkClosed();//// FIXME: Create warnings if can't create results of the given// type or concurrency//PreparedStatement pStmt = null;boolean canServerPrepare = true;// 不同的數據庫系統對sql進行語法轉換String nativeSql = getProcessEscapeCodesForPrepStmts() ? nativeSQL(sql): sql;// 判斷是否可以進行服務器端預編譯if (this.useServerPreparedStmts && getEmulateUnsupportedPstmts()) {canServerPrepare = canHandleAsServerPreparedStatement(nativeSql);}// 如果可以進行服務器端預編譯if (this.useServerPreparedStmts && canServerPrepare) {// 是否緩存了PreparedStatement對象if (this.getCachePreparedStatements()) {synchronized (this.serverSideStatementCache) {// 從緩存中獲取緩存的PreparedStatement對象pStmt = (com.mysql.jdbc.ServerPreparedStatement)this.serverSideStatementCache.remove(sql);if (pStmt != null) {// 緩存中存在對象時對原 sqlStatement 進行參數清空等((com.mysql.jdbc.ServerPreparedStatement)pStmt).setClosed(false);pStmt.clearParameters();}if (pStmt == null) {try {// 如果緩存中不存在,則調用服務器端(數據庫)進行預編譯pStmt = ServerPreparedStatement.getInstance(getLoadBalanceSafeProxy(), nativeSql,this.database, resultSetType, resultSetConcurrency);if (sql.length() < getPreparedStatementCacheSqlLimit()) {((com.mysql.jdbc.ServerPreparedStatement)pStmt).isCached = true;}// 設置返回類型以及并發類型pStmt.setResultSetType(resultSetType);pStmt.setResultSetConcurrency(resultSetConcurrency);} catch (SQLException sqlEx) {// Punt, if necessaryif (getEmulateUnsupportedPstmts()) {pStmt = (PreparedStatement) clientPrepareStatement(nativeSql, resultSetType, resultSetConcurrency, false);if (sql.length() < getPreparedStatementCacheSqlLimit()) {this.serverSideStatementCheckCache.put(sql, Boolean.FALSE);}} else {throw sqlEx;}}}}} else {// 未啟用緩存時,直接調用服務器端進行預編譯try {pStmt = ServerPreparedStatement.getInstance(getLoadBalanceSafeProxy(), nativeSql,this.database, resultSetType, resultSetConcurrency);pStmt.setResultSetType(resultSetType);pStmt.setResultSetConcurrency(resultSetConcurrency);} catch (SQLException sqlEx) {// Punt, if necessaryif (getEmulateUnsupportedPstmts()) {pStmt = (PreparedStatement) clientPrepareStatement(nativeSql, resultSetType, resultSetConcurrency, false);} else {throw sqlEx;}}}} else {// 不支持服務器端預編譯時調用客戶端預編譯(不需要數據庫 connection )pStmt = (PreparedStatement) clientPrepareStatement(nativeSql, resultSetType, resultSetConcurrency, false);}return pStmt;}流程圖如下所示:
mybatis之sql動態解析以及預編譯源碼
mybatis sql 動態解析
mybatis 在調用 connection 進行 sql 預編譯之前,會對sql語句進行動態解析,動態解析主要包含如下的功能:
-
占位符的處理
-
動態sql的處理
-
參數類型校驗
mybatis強大的動態SQL功能的具體實現就在此。動態解析涉及的東西太多,以后再討論。
總結
本文主要深入探究了 mybatis 對 #{ } 和 ${ }的不同處理方式,并了解了 sql 預編譯。
from:?https://segmentfault.com/a/1190000004617028
總結
以上是生活随笔為你收集整理的mybatis深入理解(一)之 # 与 $ 区别以及 sql 预编译的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 5.1 代码合并:Merge、Rebas
- 下一篇: 超时,重试,熔断,限流