在本教程中,您將學習如何使用PHP在MySQL中使用預(yù)處理語句。
預(yù)處理語句(也稱為參數(shù)化語句)只是一個SQL查詢模板,其中包含占位符而不是實際參數(shù)值。在執(zhí)行語句時,這些占位符將被實際值替換。
MySQLi支持使用匿名位置占位符(?),如下所示:
INSERT INTO persons (first_name, last_name, email) VALUES (?, ?, ?);
而PDO支持匿名位置占位符(?)和命名占位符。命名占位符以冒號(:)開頭,后跟標識符,如下所示:
INSERT INTO persons (first_name, last_name, email) VALUES (:first_name, :last_name, :email);
預(yù)處理語句執(zhí)行包括兩個階段:準備和執(zhí)行。
準備 - 在準備階段,將創(chuàng)建一個SQL語句模板并將其發(fā)送到數(shù)據(jù)庫服務(wù)器。服務(wù)器解析語句模板,執(zhí)行語法檢查和查詢優(yōu)化,并將其存儲以備后用。
執(zhí)行 - 執(zhí)行期間,參數(shù)值將發(fā)送到服務(wù)器。服務(wù)器從語句模板和這些值創(chuàng)建一個語句以執(zhí)行它。
預(yù)處理語句非常有用,尤其是當您多次使用不同的值(例如一系列語句)多次執(zhí)行一條特定的INSERT語句時。以下部分描述了使用它的一些主要優(yōu)點。
一個預(yù)處理語句可以高效地重復(fù)執(zhí)行同一條語句,因為該語句僅被再次解析一次,而它可以多次執(zhí)行。由于每次執(zhí)行時僅需要將占位符值傳輸?shù)綌?shù)據(jù)庫服務(wù)器,而不是傳輸完整的SQL語句,因此它還可以最大程度地減少帶寬使用。
預(yù)處理語句還提供了強大的保護,可防止SQL注入,因為參數(shù)值未直接嵌入在SQL查詢字符串中。使用不同的協(xié)議將參數(shù)值與查詢分開發(fā)送到數(shù)據(jù)庫服務(wù)器,因此不會干擾它。在解析語句模板之后,服務(wù)器直接在執(zhí)行時使用這些值。這就是為什么預(yù)處理語句不太容易出錯的原因,因此被認為是數(shù)據(jù)庫安全性中最關(guān)鍵的元素之一。
以下示例將向您展示預(yù)準備語句的實際工作方式:
<?php /* 嘗試MySQL服務(wù)器連接。 假設(shè)您正在運行MySQL。 具有默認設(shè)置的服務(wù)器(沒有密碼的用戶“root”) */ $link = mysqli_connect("localhost", "root", "", "demo"); //檢查連接 if($link === false){ die("錯誤:無法連接。 " . mysqli_connect_error()); } //使用預(yù)處理語句 $sql = "INSERT INTO persons (first_name, last_name, email) VALUES (?, ?, ?)"; if($stmt = mysqli_prepare($link, $sql)){ //將變量作為參數(shù)綁定到預(yù)處理語句 mysqli_stmt_bind_param($stmt, "sss", $first_name, $last_name, $email); /* 設(shè)置參數(shù)值并執(zhí)行,該語句再次插入另一行 */ $first_name = "Hermione"; $last_name = "Granger"; $email = "hermionegranger@mail.com"; mysqli_stmt_execute($stmt); /* 設(shè)置參數(shù)值并執(zhí)行插入行的語句 */ $first_name = "Ron"; $last_name = "Weasley"; $email = "ronweasley@mail.com"; mysqli_stmt_execute($stmt); echo "記錄插入成功。"; } else{ echo "錯誤:無法準備查詢: $sql. " . mysqli_error($link); } //關(guān)閉語句 mysqli_stmt_close($stmt); //關(guān)閉連接 mysqli_close($link); ?>
<?php /* 嘗試MySQL服務(wù)器連接。 假設(shè)您正在運行MySQL。 具有默認設(shè)置的服務(wù)器(沒有密碼的用戶“root”) */ $mysqli = new mysqli("localhost", "root", "", "demo"); //檢查連接 if($mysqli === false){ die("錯誤:無法連接。 " . $mysqli->connect_error); } // 使用預(yù)處理語句 $sql = "INSERT INTO persons (first_name, last_name, email) VALUES (?, ?, ?)"; if($stmt = $mysqli->prepare($sql)){ // 將變量作為參數(shù)綁定到預(yù)處理語句 $stmt->bind_param("sss", $first_name, $last_name, $email); /* 設(shè)置參數(shù)值并執(zhí)行。 再次執(zhí)行該語句以插入另一行 */ $first_name = "Hermione"; $last_name = "Granger"; $email = "hermionegranger@mail.com"; $stmt->execute(); /* 設(shè)置參數(shù)值并執(zhí)行 要插入行的語句 */ $first_name = "Ron"; $last_name = "Weasley"; $email = "ronweasley@mail.com"; $stmt->execute(); echo "已成功插入記錄。"; } else{ echo "錯誤:無法準備查詢:$sql. " . $mysqli->error; } //關(guān)閉語句 $stmt->close(); //關(guān)閉連接 $mysqli->close(); ?>
<?php /* 嘗試MySQL服務(wù)器連接。 假設(shè)您正在運行MySQL。 具有默認設(shè)置的服務(wù)器(沒有密碼的用戶“root”) */ try{ $pdo = new PDO("mysql:host=localhost;dbname=demo", "root", ""); // 將PDO錯誤模式設(shè)置為異常 $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } catch(PDOException $e){ die("錯誤:無法連接。 " . $e->getMessage()); } //嘗試執(zhí)行插入查詢 try{ //使用預(yù)處理語句 $sql = "INSERT INTO persons (first_name, last_name, email) VALUES (:first_name, :last_name, :email)"; $stmt = $pdo->prepare($sql); //將參數(shù)綁定到語句 $stmt->bindParam(':first_name', $first_name, PDO::PARAM_STR); $stmt->bindParam(':last_name', $last_name, PDO::PARAM_STR); $stmt->bindParam(':email', $email, PDO::PARAM_STR); /* 設(shè)置參數(shù)值并執(zhí)行, 再次執(zhí)行該語句以插入另一行 */ $first_name = "Hermione"; $last_name = "Granger"; $email = "hermionegranger@mail.com"; $stmt->execute(); /* 設(shè)置參數(shù)值并執(zhí)行 要插入行的語句 */ $first_name = "Ron"; $last_name = "Weasley"; $email = "ronweasley@mail.com"; $stmt->execute(); echo "記錄插入成功。"; } catch(PDOException $e){ die("錯誤:無法準備/執(zhí)行查詢: $sql. " . $e->getMessage()); } // 關(guān)閉語句 unset($stmt); //關(guān)閉連接 unset($pdo); ?>
如您在上面的示例中看到的,我們INSERT僅準備了一次語句,但是通過傳遞不同的參數(shù)集多次執(zhí)行了該語句。
在上面示例的SQL INSERT語句中,問號用作first_name,last_name和email字段值的占位符。
mysqli_stmt_bind_param()函數(shù)將變量綁定到占位符(?)在SQL語句模板中。占位符(?)將替換為執(zhí)行時變量中保存的實際值。作為第二個參數(shù)提供的類型定義字符串,即“sss”字符串指定每個綁定變量的數(shù)據(jù)類型為string(字符串)。
類型定義字符串指定相應(yīng)綁定變量的數(shù)據(jù)類型,參數(shù)有以下四種類型:
i - integer(整型)
d - double(雙精度浮點型)
s - string(字符串)
b - BLOB(binary large object:二進制大對象)
類型定義字符串中的綁定變量數(shù)和字符數(shù)必須與SQL語句模板中的占位符數(shù)匹配。
如果您還記得上一章,我們已經(jīng)創(chuàng)建了一個HTML表單,用于將數(shù)據(jù)插入數(shù)據(jù)庫。在這里,我們將通過執(zhí)行預(yù)處理語句來擴展該示例。您可以使用相同的HTML表單來測試以下插入腳本示例,但只需確保action在表單的屬性中使用了正確的文件名即可。
這是用于插入數(shù)據(jù)的更新的PHP代碼。如果仔細查看示例,您會發(fā)現(xiàn)我們沒有mysqli_real_escape_string()像上一章中的示例那樣使用來轉(zhuǎn)義用戶輸入。由于在預(yù)處理語句中,用戶輸入永遠不會直接替換為查詢字符串,因此不需要正確地對它們進行轉(zhuǎn)義。
<?php /* 嘗試MySQL服務(wù)器連接。 假設(shè)您正在運行MySQL。 具有默認設(shè)置的服務(wù)器(沒有密碼的用戶“root”) */ $link = mysqli_connect("localhost", "root", "", "demo"); //檢查連接 if($link === false){ die("錯誤:無法連接。 " . mysqli_connect_error()); } //使用預(yù)處理語句 $sql = "INSERT INTO persons (first_name, last_name, email) VALUES (?, ?, ?)"; if($stmt = mysqli_prepare($link, $sql)){ //將變量綁定到準備好的語句作為參數(shù) mysqli_stmt_bind_param($stmt, "sss", $first_name, $last_name, $email); //設(shè)定參數(shù) $first_name = $_REQUEST['first_name']; $last_name = $_REQUEST['last_name']; $email = $_REQUEST['email']; //嘗試執(zhí)行預(yù)處理語句 if(mysqli_stmt_execute($stmt)){ echo "記錄插入成功。"; } else{ echo "錯誤:無法執(zhí)行查詢: $sql. " . mysqli_error($link); } } else{ echo "錯誤:無法執(zhí)行查詢: $sql. " . mysqli_error($link); } // 關(guān)閉語句 mysqli_stmt_close($stmt); //關(guān)閉連接 mysqli_close($link); ?>
<?php /* 嘗試MySQL服務(wù)器連接。 假設(shè)您正在運行MySQL。 具有默認設(shè)置的服務(wù)器(沒有密碼的用戶“root”) */ $mysqli = new mysqli("localhost", "root", "", "demo"); //檢查連接 if($mysqli === false){ die("錯誤:無法連接。 " . $mysqli->connect_error); } //使用預(yù)處理語句 $sql = "INSERT INTO persons (first_name, last_name, email) VALUES (?, ?, ?)"; if($stmt = $mysqli->prepare($sql)){ //將變量作為參數(shù)綁定到預(yù)處理語句 $stmt->bind_param("sss", $first_name, $last_name, $email); //設(shè)置參數(shù) $first_name = $_REQUEST['first_name']; $last_name = $_REQUEST['last_name']; $email = $_REQUEST['email']; //嘗試執(zhí)行預(yù)處理語句 if($stmt->execute()){ echo "記錄插入成功。"; } else{ echo "錯誤:無法執(zhí)行查詢: $sql. " . $mysqli->error; } } else{ echo "錯誤:無法執(zhí)行查詢: $sql. " . $mysqli->error; } //關(guān)閉語句 $stmt->close(); //關(guān)閉連接 $mysqli->close(); ?>
<?php /* 嘗試MySQL服務(wù)器連接。 假設(shè)您正在運行MySQL。 具有默認設(shè)置的服務(wù)器(沒有密碼的用戶“root”) */ try{ $pdo = new PDO("mysql:host=localhost;dbname=demo", "root", ""); //將PDO錯誤模式設(shè)置為異常 $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } catch(PDOException $e){ die("錯誤:無法連接。 " . $e->getMessage()); } //嘗試執(zhí)行插入查詢 try{ //使用預(yù)處理語句 $sql = "INSERT INTO persons (first_name, last_name, email) VALUES (:first_name, :last_name, :email)"; $stmt = $pdo->prepare($sql); // 將參數(shù)綁定到語句 $stmt->bindParam(':first_name', $_REQUEST['first_name'], PDO::PARAM_STR); $stmt->bindParam(':last_name', $_REQUEST['last_name'], PDO::PARAM_STR); $stmt->bindParam(':email', $_REQUEST['email'], PDO::PARAM_STR); // 執(zhí)行預(yù)處理語句 $stmt->execute(); echo "記錄插入成功。"; } catch(PDOException $e){ die("錯誤:無法準備/執(zhí)行查詢 $sql. " . $e->getMessage()); } //關(guān)閉語句 unset($stmt); //關(guān)閉連接 unset($pdo); ?>
注意:盡管在預(yù)處理語句中不需要轉(zhuǎn)義用戶輸入,但是您應(yīng)始終驗證從外部源接收到的數(shù)據(jù)的類型和大小,并實施適當?shù)南拗埔苑乐瓜到y(tǒng)資源的利用。