一、事故緣起
今天構造了一個超過 50 多個參數的 SQL 插入語句,在執行的時候提示 Not all parameters were used in the SQL statement
,提示「SQL 語句中未使用所有參數」的異常,但是前前后后檢查了 SQL 語句,發現每個參數都是與相應的字段一一對應的,類似於下面這樣的代碼塊:
mydb = mysql.connect(...)
cursor = mydb.cursor()
queries = [(...), (...), (...)] # 當然這幾部分都是動態構造的
sql = "INSERT INTO `db`.`table`(`col01`, `col02`, `col03`) VALUES (%s, %s, %s)"
# 注意上條語句中 %s 和 Python 本身的字符串占位符重合,但其實不能混用
# 它可以用 ? 號代替,但是不能用 Python 的 %d, %f 等格式化符號代替
cursor.executemany(sql, queries)
cursor.close()
mydb.close()
在確定參數一個都沒有少的情況下,開始猜測是系統截斷了什么東西,然后在 StackOverflow 回答 的評論區找到了上述異常的解決方案,在聲明數據庫游標 cursor 的時候,直接加上參數 prepared=True
就搞定了,類似於下面的語句:
cursor = mydb.cursor(prepared=True)
接下來的文章就試着描述一下這個 prepared 預編譯的作用吧,文章要點參考自 PyNative,加入了一些個人見解。
二、什么是參數化查詢
參數化查詢是在原有的查詢語句中,用占位符填充各個參數,然后在執行的過程中,再將參數值傳入的一種方法。這意味着參數化查詢的被調用語句只被編譯一次,但是可以重復傳參。比如下面這條語句:
sql = "UPDATE `transaction` SET `quantity` = %s WHERE `customer_id` = %s"
在上述語句中,我們使用了 MySQL 中的 %s
占位符來傳遞參數值,當然這里用 ?
號或者關鍵詞參數 %(customer_id)s
也是可以的,通過 execute(sql, data_tuple) 能夠有效地防止 SQL 注入攻擊,當然它有更多其他好處。
三、參數化查詢的益處
參數化查詢的特性主要有以下 4 點:
- 只編譯一次:在默認的標准查詢中,MySQL 在每次執行的時候都會編譯一次語句。而在參數化查詢中,被執行的查詢語句只被編譯一次,然后在內存中恭候傳入參數進行調用,MySQL 能夠直接執行預編譯好的語句從而減少每次編譯的時間;
- 加快執行速度:尤其是需要多次調用相同的 SQL 語句時,次數越多速度提升越明顯,有點像正則表達式的 compile 預編譯;
- 能夠使用不同的數據執行相同的操作:假設你擁有幾百行的數據要插入到數據庫表中,使用參數化查詢能夠存入不同的字段值;
- 防止 SQL 注入攻擊,有效提升數據安全。
四、如何使用參數化查詢
我們使用 mysql.connection.cursor(prepared=True) 聲明游標,就可以使用參數化查詢執行預編譯語句了,它本身是一個 MySQLCursorPrepared 類,繼承自 MySQLCursor 類對象。
from mysql import connector
connection = connector.connect(
host="localhost",
port="3306",
user="MoonYear530",
password="StanleyBlog",
database="query_test"
)
cursor = connection.cursor(prepared=True)
# 上一條語句也可以擬寫如下:
# cursor = connection.cursor(cursor_class=MySQLCursorPrepared)
這樣,在接下來的每一次調用中,傳入的參數雖然不同,但是在准備執行時發現 SQL 語句是一樣的時候,就會跳過編譯了。
五、使用參數化查詢更新數據的示例
from mysql import connector
from mysql.connector import Error
try:
connection = connector.connect(
host="localhost",
port="3306",
user="MoonYear530",
password="StanleyBlog",
database="query_test"
) # 創建數據庫連接
cursor = connection.cursor(prepared=True) # 聲明數據游標
sql = """UPDATE `transaction`
SET `update_date` = %s, `quantity` = %s
WHERE `customer_id` = %s""" # 被調用 SQL 語句
data_tuple = ("2020-09-20", 16530, "SZ0198") # 傳入的參數元組
cursor.execute(sql, data_tuple) # 執行 SQL 語句
connection.commit() # 提交事務
print("恭喜,數據更新成功!")
except Error as error:
connection.rollback() # 執行失敗,數據回滾
print(f"參數化查詢執行異常:{error}")
finally:
if connection.is_connected():
cursor.close() # 關閉數據游標
connection.close() # 關閉數據庫連接
print("老鐵,數據庫連接已關閉。")
以上代碼只是一個簡短的示例,需要按照工程內容適當修改,祝一切順利,代碼無 Bug,身體倍棒,吃嘛嘛香~