PHP 騙你,PDO prepare 並沒有準備好

ChiVincent
May 26, 2018 · 5 min read

被 PHP 騙也不是一天兩天的事了,習慣成自然

大部份的 Modern PHPer 都會告訴你:用 PDO 取代 mysqli相關函式吧,不僅支援 ,而且還有多種 Driver 可以隨時切換不同的 Database。 至於那個 mysql 就讓它死在歷史的洪流就好,死人就該好好躺著,不要出來假鬼假怪。

包括 所使用的 ,或是 Laravel 底層的 Eloquent ORM,無不使用 PDO 作為其底層的實作。

我們先來看看 PDO 存取資料的標準範例:

<?phptry {
    $dbh = new PDO(
        'mysql:host=127.0.0.1;dbname=test;charset=utf8mb4', 
        'root', 
        'root'
    );    $sth = $dbh->prepare('SELECT * FROM `users` WHERE `role` = ?');
    $sth->bindValue(1, $_GET['role']);    $result = $sth->execute();    var_dump($result);
} catch (PDOException $exception) {
    die ("Something wrong: {$exception->getMessage()}");
}

常識告訴我們,如果打算把使用者輸入(包括但不限於 $_GET$_POST$_COOKIE$_SERVER )放進 SQL Query 中,用 prepare 這個函式會比用什麼 addslashes 之類的函式來得安全。

而有許多的初學者教學書籍中,也會告訴你如果使用 prepare 的話,當同一句 SQL Query 將被執行很多次,其效能會提升:因為 SQL Statement 先轉化為樣板(template),之後再對其賦值(assignment)

然而,這一切其實都是假像。在預設設定下,如果使用的是 MySQL Driver,PHP 的 prepare 其實只是在綁定值之後對整個 SQL 語句做 mysql_real_escape_string ,而不是依照預期的會對 Database 先做 Request Prepare Statement,然後再做 Prepare Execute Statement。

註:PostgresQL 也有類似的行為。

這是因為 PHP 為了避免部份 Database 或 Driver 不支援 Prepared Statement,於是預設採用了「模擬」的方式,假裝自己是使用 Prepare Statement。

2019/01/21 補充:據 contributor 所述,SQL level 的 prepared statement 是相當影響效能的,詳情可以參見 這篇文章。

正因為如此,在預設設定之下,就算該 SQL 會被執行多次,使用 prepare 仍是無法提升效能的。

實驗

以下我做了一個實驗來驗證這篇文章的說法。

  • PHP 7.2.5
  • MySQL 8.0.11 (Running in Docker)
<?phptry {
    $dbh = new PDO(
        'mysql:host=127.0.0.1;dbname=test;charset=utf8mb4', 
        'root', 
        'root'
    );    $sth = $dbh->prepare('SELECT * FROM `users` WHERE `id` = ?');
    $sth->bindValue(1, $_GET['id']);    $sth->execute();
    $result = $sth->fetch();    var_dump($result);
} catch (PDOException $e) {
 die("Something wrong: {$e->getMessage()}");
}

分析封包,會看到以下的結果

在 PDO::ATTR_EMULATE_PREPARES = true 時(預設)

如果在第 9 行加入 $dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

Request Prepare Statement(左圖)及 Request Execute Statement (右圖)

結論

如果你很確定你的 Driver 及 Database 是支援 Prepared Statement 的,建議應該要設定 PDO::ATTR_EMULATE_PREPARESfalse

,這也是為什麼比起從頭建立框架/元件,用一個已經足夠成熟的框架/元件會讓自己的應用程式效率更高(也可能更加安全)

wetprogrammer

A web programmer, using modern PHP, Rust and Golang

ChiVincent

Written by

http://chivincent.net/

wetprogrammer

A web programmer, using modern PHP, Rust and Golang