1. 程式人生 > >mybatis深入理解之 # 與 $ 區別以及 sql 預編譯

mybatis深入理解之 # 與 $ 區別以及 sql 預編譯

tcl nec from esql 校驗 ntp code 理解 替換字符串

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 來抽象預編譯語句,使用預編譯

  1. 預編譯階段可以優化 sql 的執行。

    預編譯之後的 sql 多數情況下可以直接執行,DBMS 不需要再次編譯,越復雜的sql,編譯的復雜度將越大,預編譯階段可以合並多次操作為一個操作。

  2. 預編譯語句對象可以重復利用。

    把一個 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 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之sql動態解析以及預編譯源碼

mybatis sql 動態解析

mybatis 在調用 connection 進行 sql 預編譯之前,會對sql語句進行動態解析,動態解析主要包含如下的功能:

  • 占位符的處理

  • 動態sql的處理

  • 參數類型校驗

mybatis強大的動態SQL功能的具體實現就在此。動態解析涉及的東西太多,以後再討論。

總結

本文主要深入探究了 mybatis 對 #{ } 和 ${ }的不同處理方式,並了解了 sql 預編譯。

mybatis深入理解之 # 與 $ 區別以及 sql 預編譯