mybatis 中使用 sqlMap 進行 sql 查詢時,常常須要動態傳遞參數,例如咱們須要根據用戶的姓名來篩選用戶時,sql 以下:java
select * from user where name = "ruhua";
上述 sql 中,咱們但願 name 後的參數 "ruhua" 是動態可變的,即不一樣的時刻根據不一樣的姓名來查詢用戶。在 sqlMap 的 xml 文件中使用以下的 sql 能夠實現動態傳遞參數 name:mysql
select * from user where name = #{name};
或者sql
select * from user where name = '${name}';
對於上述這種查詢狀況來講,使用 #{ } 和 ${ } 的結果是相同的,可是在某些狀況下,咱們只能使用兩者其一。數據庫
動態 SQL 是 mybatis 的強大特性之一,也是它優於其餘 ORM 框架的一個重要緣由。mybatis 在對 sql 語句進行預編譯以前,會對 sql 進行動態解析,解析爲一個 BoundSql 對象,也是在此處對動態 SQL 進行處理的。緩存
在動態 SQL 解析階段, #{ } 和 ${ } 會有不一樣的表現:服務器
#{ } 解析爲一個 JDBC 預編譯語句(prepared statement)的參數標記符。mybatis
例如,sqlMap 中以下的 sql 語句併發
select * from user where name = #{name};
解析爲:框架
select * from user where name = ?;
一個 #{ } 被解析爲一個參數佔位符 ?
。ide
而,
${ } 僅僅爲一個純碎的 string 替換,在動態 SQL 解析階段將會進行變量替換
例如,sqlMap 中以下的 sql
select * from user where name = '${name}';
當咱們傳遞的參數爲 "ruhua" 時,上述 sql 的解析爲:
select * from user where name = "ruhua";
預編譯以前的 SQL 語句已經不包含變量 name 了。
綜上所得, ${ } 的變量的替換階段是在動態 SQL 解析階段,而 #{ }的變量的替換是在 DBMS 中。
一、能使用 #{ } 的地方就用 #{ }
首先這是爲了性能考慮的,相同的預編譯 sql 能夠重複利用。
其次,${ } 在預編譯以前已經被變量替換了,這會存在 sql 注入問題。例如,以下的 sql,
select * from ${tableName} where name = #{name}
假如,咱們的參數 tableName 爲 user; delete user; --
,那麼 SQL 動態解析階段以後,預編譯以前的 sql 將變爲
select * from user; delete user; -- where name = ?;
--
以後的語句將做爲註釋,不起做用,所以原本的一條查詢語句偷偷的包含了一個刪除表數據的 SQL!
二、表名做爲變量時,必須使用 ${ }
這是由於,表名是字符串,使用 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 語句和參數給 DBMS 以前對 sql 語句進行編譯,這樣 DBMS 執行 sql 時,就不須要從新編譯。
JDBC 中使用對象 PreparedStatement 來抽象預編譯語句,使用預編譯
預編譯階段能夠優化 sql 的執行。
預編譯以後的 sql 多數狀況下能夠直接執行,DBMS 不須要再次編譯,越複雜的sql,編譯的複雜度將越大,預編譯階段能夠合併屢次操做爲一個操做。
預編譯語句對象能夠重複利用。
把一個 sql 預編譯後產生的 PreparedStatement 對象緩存下來,下次對於同一個sql,能夠直接使用這個緩存的 PreparedState 對象。
mybatis 默認狀況下,將對全部的 sql 進行預編譯。
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 necessary if (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 necessary if (getEmulateUnsupportedPstmts()) { pStmt = (PreparedStatement) clientPrepareStatement(nativeSql, resultSetType, resultSetConcurrency, false); } else { throw sqlEx; } } } } else { // 不支持服務器端預編譯時調用客戶端預編譯(不須要數據庫 connection ) pStmt = (PreparedStatement) clientPrepareStatement(nativeSql, resultSetType, resultSetConcurrency, false); } return pStmt; }
流程圖以下所示:
mybatis 在調用 connection 進行 sql 預編譯以前,會對sql語句進行動態解析,動態解析主要包含以下的功能:
佔位符的處理
動態sql的處理
參數類型校驗
mybatis強大的動態SQL功能的具體實現就在此。動態解析涉及的東西太多,之後再討論。
本文主要深刻探究了 mybatis 對 #{ } 和 ${ }的不一樣處理方式,並瞭解了 sql 預編譯。