【轉】Python mysql 引數替換的坑
文章轉自 https://blog.xupeng.me/2013/09/25/mysqldb-args-processing/ 感謝原作者分享
前幾天又有同事掉進了給 SQL 的 IN 條件傳參的坑,就像 SELECT col1, col2 FROM table1 WHERE id IN (1, 2, 3) 這類 SQL,如果是一個可變的列表作為 IN 的引數,那這個引數應該怎麼傳呢?
我見過至少這麼幾種:
1 2 |
|
這種方式是語法錯誤的,原因是 MySQLdb 做字串格式化時佔位符和引數個數不匹配。
1 2 |
|
這種方式語法是正確的,但語義是錯誤的,因為生成的 SQL 是 SELECT col1, col2 FROM table1 WHERE id IN ((‘1’, ‘2’, ‘3’))
1 2 3 |
|
這種方式語義也是錯誤的,因為生成的 SQL 是 SELECT col1, col2 FROM table1 WHERE id IN (‘1,2,3’)
這三種是第一次使用 MySQLdb 給 IN 傳參時犯的最多的錯誤,大多數人遇到第一種錯和掉進後兩個坑之後,轉而採用了下面的方式:
1 2 3 |
|
這個方式對於可信的引數(比如自己生成的列表:range(1, 10, 2)
)來說可以用,但由於引數未經 escape,對於從使用者端接受的不可信引數來說,存在 SQL 注入的風險。
嚴防 SQL 注入的問題時刻都不能鬆懈,於是就有了這樣的改進版本:
1 2 3 |
|
這個方式控制了 SQL 注入問題的滋生,但由於 cursor.connection.literal
是內部介面,並不推薦從外部使用。
然後就有了這樣的方式:
1 2 3 |
|
這個方式是先生成與引數個數相同的 %s 佔位,拼出 ‘SELECT col1, col2 FROM table1 WHERE id IN (%s,%s,%s)’ 這樣的 SQL,然後使用安全的方式來傳參。
就是想傳一個引數而已,怎麼會這麼麻煩呢?觸令喪慘!
更正:以下劃線內容為未經充分測試的錯誤結論,僅做記錄:
一直以為 MySQLdb 是不支援給 IN 傳參的,直到這次又有同事掉坑我才讀了 MySQLdb escape 部分的程式碼,然後發現,MySQLdb 是在很多型別的 Python object 和 SQL 支援的型別之間做自動轉換的,比如 MySQLdb 會對 list 和 tuple 內的元素逐個進行 escape,生成一個 tuple,因此這才是正確的給 IN 傳參的方式:
|
|
可以把 MySQLdb 處理引數的過程簡化描述為:
對引數 (id_list,) 做 escape 得到 ((‘1’, ‘2’, ‘3’),)用 escape 過的引數對 SQL 進行格式化:’SELECT col1, col2 FROM table1 WHERE id IN %s’ % ((‘1’, ‘2’, ‘3’),),得到完整 SQL:’SELECT col1, col2 FROM table1 WHERE id IN (‘1’, ‘2’, ‘3’)
整理一下口訣:IN 的引數和其他引數一樣,是一個整體,就要不要對屬於引數一部分的 ()
念念不忘了……
總結一下評論中對這個方法提出的問題:
- 如果引數列表只有一個元素,比如
cursor.execute('SELECT col1, col2 FROM table1 WHERE id IN %s', ([1],))
,生成的 SQL 是SELECT col1, col2 FROM table1 WHERE id IN ('1',)
,是語法錯誤的 - 對列表內元素做 esacpe 時增加的引號會被留下,如果列表元素是字串,結果會是錯誤的,比如
cursor.execute('SELECT col1, col2 FROM table1 WHERE id IN %s', (["1", "2"],))
生成的 SQL 是SELECT col1, col2 FROM table1 WHERE id IN ("'1'", "'2'")
,而對於數字引數恰好能正確工作的原因是,在執行 SQL 時如果列定義是 int 而傳參為字串,MySQL 會做隱式型別轉換(Type Conversion in Expression Evaluation)。
MySQLdb 支援對各種型別的 Python object 進行轉換和 escape,感興趣的同學可以看看 MySQLdb.converters
和 _mysql.c
中 *_escape*
系列的函式,另外 MySQLdb 也支援自定義轉換規則,參見 MySQLdb.connect
的 conv
引數。