PHP Conference Japan 2024

多個語句

MySQL 可選擇性地在一個語句字串中包含多個語句,但這需要特殊處理。

多個語句或多查詢必須使用 mysqli::multi_query() 執行。語句字串中的個別語句以分號分隔。然後,必須擷取執行語句返回的所有結果集。

MySQL 伺服器允許在一個多語句中包含返回結果集的語句和不返回結果集的語句。

範例 #1 多個語句

<?php

mysqli_report
(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$mysqli = new mysqli("example.com", "user", "password", "database");

$mysqli->query("DROP TABLE IF EXISTS test");
$mysqli->query("CREATE TABLE test(id INT)");

$sql = "SELECT COUNT(*) AS _num FROM test;
INSERT INTO test(id) VALUES (1);
SELECT COUNT(*) AS _num FROM test; "
;

$mysqli->multi_query($sql);

do {
if (
$result = $mysqli->store_result()) {
var_dump($result->fetch_all(MYSQLI_ASSOC));
$result->free();
}
} while (
$mysqli->next_result());

以上範例會輸出

array(1) {
  [0]=>
  array(1) {
    ["_num"]=>
    string(1) "0"
  }
}
array(1) {
  [0]=>
  array(1) {
    ["_num"]=>
    string(1) "1"
  }
}

安全性考量

API 函式 mysqli::query()mysqli::real_query() 並不會設定伺服器啟用多重查詢所需的連線旗標。多個敘述會使用額外的 API 呼叫,以減少意外 SQL 注入攻擊的損害。攻擊者可能會嘗試新增諸如 ; DROP DATABASE mysql; SELECT SLEEP(999) 之類的敘述。如果攻擊者成功將 SQL 新增至敘述字串,但未使用 mysqli::multi_query(),伺服器將不會執行注入的惡意 SQL 敘述。

範例 #2 SQL 注入

<?php
mysqli_report
(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$mysqli = new mysqli("example.com", "user", "password", "database");
$result = $mysqli->query("SELECT 1; DROP TABLE mysql.user");
?>

以上範例會輸出

PHP Fatal error:  Uncaught mysqli_sql_exception: You have an error in your SQL syntax; 
check the manual that corresponds to your MySQL server version for the right syntax to 
use near 'DROP TABLE mysql.user' at line 1

預備敘述 (Prepared statements)

不支援搭配預備敘述使用多重敘述。

另請參閱

新增筆記

使用者貢獻的筆記 1 則筆記

velthuijsen
6 年前
建議對範例 1 的改進。

原因
Multi_query 僅在返回資料/結果集時才返回非 false 的回應,並且僅檢查輸入的第一個查詢。將第一個 SELECT 查詢與 INSERT 查詢交換將導致範例提前退出,並顯示訊息「多重查詢失敗:(0)」。
該範例假設一旦第一個查詢未失敗,則其他查詢也已成功。或者更確切地說,它只是在沒有報告第一個查詢之後的其中一個查詢失敗的情況下退出,因為如果查詢失敗,next_result 會返回 false。

範例中的更改是在建立字串 $sql 之後進行的。

<?php
$mysqli
= new mysqli("example.com", "user", "password", "database");
if (
$mysqli->connect_errno) {
echo
"Failed to connect to MySQL: (" . $mysqli->connect_errno . ") " . $mysqli->connect_error;
}

if (!
$mysqli->query("DROP TABLE IF EXISTS test") || !$mysqli->query("CREATE TABLE test(id INT)")) {
echo
"Table creation failed: (" . $mysqli->errno . ") " . $mysqli->error;
}

$sql = "SELECT COUNT(*) AS _num FROM test; ";
$sql.= "INSERT INTO test(id) VALUES (1); ";
$sql.= "SELECT COUNT(*) AS _num FROM test; ";

// changes to example 1 start here

// don't bother checking the result from multi_query since it will return false
// if the first query does not return data even if the query itself succeeds.
$mysqli->multi_query($sql);

do
// while (true); // exit only on error or when there are no more queries to process
{
// check if query currently being processed hasn't failed
if (0 !== $mysqli->errno)
{
echo
"Multi query failed: (" . $mysqli->errno . ") " . $mysqli->error;
break;
}

// store and possibly process result of the query,
// both store_result & use_result will return false
// for queries that do not return results (INSERT for example)
if(false !== ($res = $mysqli->store_result() )
{
var_dump($res->fetch_all(MYSQLI_ASSOC));
$res->free();
}

// exit loop if there ar no more queries to process
if (false === ($mysqli->more_results() )
{
break;
}

// get result of the next query to process
// don't bother to check for success/failure of the result
// since at the start of the loop there is an error check &
// report block.
$mysqli->next_result()

} while (
true); // exit only on error or when there are no more queries to process
?>

請注意,正常的 while ($mysqli->more_results() && $mysqli->next_result() 已被兩個檢查和 while (true) 取代;
這是由於如果相關查詢失敗,next_result 將返回 false 的「問題」所致。
因此,需要在 while 迴圈之後進行最後一次檢查以檢查是否有錯誤,或者必須將不同的操作分開。
範例中的更改執行了拆分。
To Top