在MyBatis框架的使用過程中,SQL映射文件的編寫是核心工作之一。而#{}和${}這兩種參數占位符語法,雖然看起來相似,卻有著本質的區別。正確理解和使用它們,不僅關系到應用程序的安全性,還會影響系統性能。本文將全面剖析這兩種語法的區別、實現原理、使用場景以及最佳實踐。
一、基本概念與語法
1.1 #{}語法
#{}是MyBatis中的預編譯占位符,也稱為參數標記。它的基本形式如下:
<select id="findUserById" resultType="User">SELECT * FROM users WHERE id = #{userId}
</select>
1.2 ${}語法
${}是MyBatis中的字符串替換占位符,也稱為非轉義字符串替換。它的基本形式如下:
<select id="findUsersByTable" resultType="User">SELECT * FROM ${tableName} WHERE status = 1
</select>
二、底層實現原理
2.1 #{}的工作原理
當MyBatis遇到#{}時,會進行以下處理:
解析階段:MyBatis解析SQL映射文件時,識別出#{}標記
參數處理:運行時將參數值通過PreparedStatement的set方法設置
SQL生成:最終生成帶有"?"的預編譯SQL語句
例如上面的例子會生成:
SELECT * FROM users WHERE id = ?
然后通過PreparedStatement的setInt/setString等方法設置參數值。
2.2 ${}的工作原理
對于${}的處理則完全不同:
直接替換:MyBatis在SQL解析階段就直接將${}替換為實際的參數值
字符串拼接:替換后的SQL語句是通過字符串拼接而成的
直接執行:最終生成的是完整的SQL語句,而非預編譯語句
例如,如果tableName="users",生成的SQL就是:
SELECT * FROM users WHERE status = 1
三、核心區別對比
3.1 安全性對比
特性 | #{} | ${} |
---|---|---|
SQL注入防護 | 安全,能防止SQL注入 | 不安全,存在SQL注入風險 |
實現方式 | 參數化查詢 | 字符串拼接 |
#{}示例:
String sql = "SELECT * FROM users WHERE name = #{name}";
// 即使用戶輸入 name = "' OR '1'='1"
// 實際執行:SELECT * FROM users WHERE name = ?
// 參數值會被正確處理,不會導致注入
${}示例:
String sql = "SELECT * FROM users WHERE name = '${name}'";
// 如果用戶輸入 name = "' OR '1'='1"
// 實際執行:SELECT * FROM users WHERE name = '' OR '1'='1'
// 這將返回所有用戶數據,造成SQL注入
3.2 性能對比
特性 | #{} | ${} |
---|---|---|
數據庫優化 | 支持預編譯,可緩存執行計劃 | 每次都是新SQL,無法緩存 |
網絡傳輸 | 只需傳輸參數 | 需要傳輸完整SQL |
編譯次數 | 一次編譯多次執行 | 每次都需要重新編譯 |
3.3 使用場景對比
場景 | #{} | ${} | 說明 |
---|---|---|---|
普通參數值 | ? | ? | 推薦使用#{} |
表名 | ? | ? | 動態表名必須使用${} |
列名 | ? | ? | 動態列名必須使用${} |
ORDER BY子句 | ? | ? | 動態排序需謹慎使用 |
GROUP BY子句 | ? | ? | 動態分組需謹慎使用 |
LIKE模糊查詢 | ? | ? | 需特殊處理通配符 |
四、深入應用場景
4.1 必須使用#{}的場景
所有用戶輸入的參數值
WHERE username = #{username} AND password = #{password}
數值型參數
WHERE age > #{minAge} AND age < #{maxAge}
日期型參數
WHERE create_time > #{startDate}
4.2 可能需要使用${}的場景
動態表名?
SELECT * FROM ${tableName}
適用于分表場景,如表名按年份分表:user_2022, user_2023等
動態列名?
SELECT ${columns} FROM users
適用于動態選擇返回字段的場景
ORDER BY排序?
ORDER BY ${sortColumn} ${sortOrder}
但更安全的做法是:
<choose><when test="sortColumn == 'name'">ORDER BY name</when><when test="sortColumn == 'age'">ORDER BY age</when><otherwise>ORDER BY id</otherwise> </choose>
4.3 特殊場景處理
LIKE模糊查詢的正確寫法:
錯誤方式:
WHERE name LIKE '%${name}%'
正確方式:
// Java代碼中處理參數
String nameParam = "%" + name + "%";
WHERE name LIKE #{nameParam}
或使用SQL函數:
WHERE name LIKE CONCAT('%', #{name}, '%')
五、最佳實踐建議
5.1 安全性實踐
默認使用#{}:除非確有必要,否則總是使用#{}
嚴格過濾${}參數:使用${}時,必須對參數值進行白名單驗證
// 驗證表名是否合法 if (!isValidTableName(tableName)) {throw new IllegalArgumentException("Invalid table name"); }
避免用戶輸入直接用于${}:特別是排序、分組等場景
5.2 性能優化實踐
優先使用#{}:利用預編譯語句的緩存優勢
減少${}使用頻率:對于頻繁調用的SQL,避免使用${}導致無法緩存執行計劃
批量處理動態SQL:對于必須使用${}的場景,考慮批量處理減少SQL解析次數
5.3 代碼可維護性實踐
明確注釋:在使用${}的地方添加注釋說明原因
<!-- 必須使用${}因為表名是動態的 --> SELECT * FROM ${tableName}
集中管理:將動態部分集中管理,便于維護和安全檢查
單元測試:為使用${}的SQL編寫額外的安全測試用例
六、常見問題解答
Q1:為什么ORDER BY不能使用#{}?
A:因為#{}會給參數值添加引號,例如:
ORDER BY 'name' 'DESC' -- 錯誤的SQL語法
而正確的應該是:
ORDER BY name DESC
Q2:什么情況下必須使用${}?
A:當SQL語句的非參數部分需要動態變化時,如:
動態表名
動態列名
SQL關鍵字(如ASC/DESC)
Q3:如何安全地使用${}?
A:可以采取以下措施:
使用白名單驗證參數值
避免直接使用用戶輸入
對參數值進行轉義處理
最小化使用范圍
總結
#{}和${}在MyBatis中扮演著不同的角色:
#{}?是安全的參數占位符,適用于幾乎所有參數值的場景,能防止SQL注入,性能更好。
${}?是字符串替換,適用于SQL語句本身需要動態變化的場景,但存在安全風險,應當謹慎使用。
在實際開發中,我們應該遵循以下原則:
默認使用#{}
謹慎評估${}的使用必要性
對必須使用${}的場景實施嚴格的安全控制
編寫清晰的文檔和注釋說明使用原因
正確理解和使用這兩種占位符,將使你的MyBatis應用更加安全、高效和可維護。