PHP 資料庫擴充之 PDO

VankeGabe發表於2019-07-12

PDO

js經常操作dom元素,相比,php最常操作的就是跟資料庫的互動,我們寫很多邏輯,經常都是需要讀寫資料,所以在php訪問mysql,已經有了三個成熟的擴充,分別是pdo,mysqli,mysql,但是mysql在php7已經被移除了;

pdo是一個資料抽象層,並沒有實現和資料庫的互動,而是統一定義了一套方法,具體的運算元據功能需要引入pdo資料庫驅動來實現;當我們需要和mysql互動的時候,就引入pdo_mysql驅動,當我們需要和sqlite互動的時候,就引入pdo_sqlite驅動;這就是他和mysqli最大的不同,mysqli是隻針對mysql資料庫而開發出來的擴充,而pdo可以相容多個資料庫,靈活性比較強,但是效能會比mysqli差一點。laravel框架資料庫底層也是對pdo進行封裝。

安裝和配置

從php5.1起pdo和pdo_sqlite擴充是預設安裝的,但是我們經常操作的資料庫是mysql,所以我們第一步就是學習如何安裝pdo_mysql擴充 驅動

  • 在原始碼安裝的時候,捎上pdo_mysql
    首先,我們應該掌握php原始碼安裝的方法,如果不清楚的同學,可以去先去學習下,我們都知道原始碼安裝的過程需要執行一步 ./configure --prefix=/usr/local/php 來生成編譯檔案,在這裡我就只指定了一個prefix引數,來指定php的安裝路徑,所以在這裡 我們可以加上引數 --with-pdo-mysql引數,指定在安裝php的時候,pdo_mysql擴充驅動也載入進來
    PHP 資料庫擴充之 PDO
    編譯安裝後,我們可以通過php -m或者php -i | grep pdo_mysql ,或者如果你配好ngixn和php互動的web服務,通過執行 php檔案中的方法phpinfo()來檢視是否載入成功

  • 如果我們是在安裝完php後,想載入pdo_mysql擴充的話,可以通過原始碼包裡面有個專門放置擴充檔案原始碼的地方,進行編譯安裝,或者通過pecl install pdo_mysql 安裝,也可以到https://pecl.php.net/package/PDO_MYSQL將原始碼下載下來,然後再進行編譯安裝,具體安裝擴充的方式,不懂的同學可以去學習下。這裡就不詳解了。需要注意一點是,這種安裝方式,需要在編譯安裝好擴充後,在php的ini配置檔案,加上擴充 extension=pdo_mysql,然後重啟php程式,如果是命令列執行的話,就不需要重啟,實時載入配置。

pdo配置的話,基本上沒啥配置,在pdo底層原始碼裡,主要有幾個類:PDO、PDOException、PDOStatement。其中PDOException主要是連線資料庫錯誤時和設定PDO錯誤模式時會丟擲異常,重點是學習PDO和PDOStatement裡面的方法如何運用,至於PDO裡面有大一堆常量,有興趣的同學可以瞭解下

使用

連線資料庫

當我們瞭解完PDO和安裝完擴充之後,我們第一步就是連線上mysql資料庫,我們知道mysql的架構模式是C/S,客戶端和服務端互動,我們通常連線mysql是通過,mysql -h 127.0.0.1 -u root -proot 來連線資料庫,這是mysql本身自帶的客戶端。所以pdo_mysql驅動擴充就是用來代替mysql客戶端,實現與mysql的互動。

$dsn = 'mysql:host=127.0.0.1;dbname=test';
$user = 'root';
$pass = 'root';
$dbh = new PDO($dsn, $user, $pass);

上面就是簡單的連線,在dsn資料來源名稱中我們指定了使用pdo_mysql驅動,主機和資料庫名稱,其次就是使用者和密碼,當我們連線成功的時候,會返回一個pdo物件;失敗的時候,則會丟擲一個PDOException異常,如果我們不捕捉異常的話,就有可能洩露資料庫使用者和密碼;
PHP 資料庫擴充之 PDO
所以我們通常都會捕捉這個異常

$dsn = 'mysql:host=172.17.0.3;dbname=laravel';
$user = 'root ';
$pass = 'root';
try {    
$dbh = new PDO($dsn, $user, $pass);
} catch (PDOException $e) {    
die('資料庫連線失敗');
}

當我們new一個pdo物件的時候,這個物件就是一個資料庫連線控制程式碼,如果對這個物件進行多次引用的話,資料庫連線還是原來這個,同理,只有當這個物件的所有引用都被銷燬的時候,這個資料庫連線才會斷開。怎麼驗證這個原理呢?mysql中有一個show processlist 命令來展示現在當前mysql伺服器上的所有執行緒連線池

$dsn = 'mysql:host=172.17.0.3;dbname=laravel';
$user = 'root ';
$pass = 'root';
try {    
$dbh = new PDO($dsn, $user, $pass);
} catch (PDOException $e) {    
die('資料庫連線失敗');
}
sleep(15);
file_put_contents('./test.log','開始引用物件了');
$dbh1 = $dbh;
$dbh2 = $dbh;
sleep(15);

在執行上面那段程式碼的時候,先使用 show processlist檢視當前mysql伺服器上面的執行緒連線池
PHP 資料庫擴充之 PDO
然後執行程式碼,卡在15秒之前趕緊執行一次 show processlist
PHP 資料庫擴充之 PDO
發現多了一個資料庫連線,沒錯,就是第一次資料庫連線,等到日誌產生了開始引用物件了內容的時候,再執行一次 show processlist
PHP 資料庫擴充之 PDO
發現並沒有產生新的資料庫連線控制程式碼,因為我們只是對物件進行引用,指向的都是同一個資料庫連線。所以要斷開資料庫連線很簡單,把這個物件的所有引用主動的銷燬或者指令碼執行結束的時候會自動銷燬掉。
PHP 資料庫擴充之 PDO
那麼問題來了?實際中,在很多地方,比如各種方法都需要運算元據,就會new一個資料庫連線控制程式碼,如果在一個執行緒中,很多地方都new一個新的資料庫物件的話,那麼就會產生很多的連線,造成大量資源消耗,並且這是不可控的。

$dsn = 'mysql:host=172.17.0.3;dbname=laravel';
$user = 'root';
$pass = 'root';
try {    
$dbh0 = new PDO($dsn, $user, $pass);    
$dbh1 = new PDO($dsn, $user, $pass);    
$dbh2 = new PDO($dsn, $user, $pass);    
$dbh3 = new PDO($dsn, $user, $pass);    
$dbh4 = new PDO($dsn, $user, $pass);    
$dbh5 = new PDO($dsn, $user, $pass);    
$dbh6 = new PDO($dsn, $user, $pass);    
$dbh7 = new PDO($dsn, $user, $pass);    
$dbh8 = new PDO($dsn, $user, $pass);    
$dbh9 = new PDO($dsn, $user, $pass);
} catch (PDOException $e) {    
die('資料庫連線失敗');
}
sleep(30);

在執行上面那個指令碼之前,使用 show processlist檢視當前連線執行緒池
PHP 資料庫擴充之 PDO
執行指令碼後,再次執行show processlist
PHP 資料庫擴充之 PDO
產生了10個資料庫連線,那麼解決辦法就是使用單例模式,使在一個執行緒或者請求中,資料庫的連線只會產生一個,具體可以看後面,我使用了pdo,運用單例模式封裝了一個mysql類。

事務

在學習使用pdo類其他方法前,先學習下pdo類中的事務相關的四個方法:beginTransaction()、commit()、rollBack()、inTransaction(),那麼首先我們應該先了解mysql事務,以及這四個方法代表mysql哪條命令;事務支援四大特性(ACID):原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)以及永續性(Durability),一個事務的特點就是原子性,要麼都執行成功,要麼都失敗。事務在實際應用中非常重要,可以避免異常資料的產生,導致資料不一致。具體詳細的可以自行去學習mysql的事務原理。
方法對映mysql命令
beginTransaction():start transaction
commit():commit
rollBack():rollback

// 事務都會跟php的拋異常結合,拋異常通常也是解決流程一致性問題
$dsn = 'mysql:host=172.17.0.3;dbname=laravel';
$user = 'root';
$pass = 'root';
try {    
$dbh = new PDO($dsn, $user, $pass);
} catch (PDOException $e) {    
die('資料庫連線失敗');
}
$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$dbh->beginTransaction();
try {    
$dbh->exec('insert into `user` (`name`, `age`) value("Test", 27)');    
$dbh->exec('insert into `address` (`user_id`, `city`) value("Test", 
"SZ")'); // user_id列是int型別,插入了字串會報錯    
$dbh->commit();
} catch (PDOException $e) {   
$dbh->rollBack();    
die($e->getMessage());
}

報錯:
SQLSTATE[HY000]: General error: 1366 Incorrect integer value: 'Test' for column 'user_id' at row 1
然後去查下資料庫使用者並沒有插入進去,所以該事務被回滾了。保證了資料一致性。

預處理語句

平常我們都在寫業務,很多人沒去考慮安全性和效率問題。去執行一條sql的時候,可能更多的人會直接選擇pdo類中的query()、exec()讀寫兩個操作;而不會採用prepare()方法來產生一個預處理語句物件,然後再進行繫結引數,再執行sql。我們先看下兩種方法都是怎麼用的。

    ##  非預處理
    $dsn = 'mysql:host=172.17.0.4;dbname=test';
    $user = 'root';
    $pass = 'root';
    try {    
        $dbh = new PDO($dsn, $user, $pass);
    } catch (PDOException $e) {   
        die('資料庫連線失敗');
    }
    $id = $_GET['id'];
    $data = $dbh->query('select * from `users` where `id` > '. $id);
    foreach ($data as $user) {    
        echo $user['id'] . ':' . $user['name'] . '<br/>';
    }

    ##  預處理語句
    $dsn = 'mysql:host=172.17.0.4;dbname=test';
    $user = 'root';
    $pass = 'root';
    try {    
    $dbh = new PDO($dsn, $user, $pass);
    } catch (PDOException $e) {    
    echo 'Connect error:'. $e->getMessage();    
    die;
    }
    $stmt = $dbh->prepare('select * from `users` where `id` > :id');
    $stmt->bindParam(':id', $id);
    $id = $_GET['id'];
    $stmt->execute();
    while ($row = $stmt->fetchObject()) {   
    echo $row->id . ':'. $row->name . '<br/>';
    }

這兩種方式執行的效率是差不多的

  • 如果我們傳入引數 id=1;truncate table address;這個時候非預處理語句會把address表資料給清空了,但是預處理語句則可以不會;這就是預處理語句可以防止sql注入,當然非預處理語句,如果引數是C端(使用者)輸入的話,我們則可以使用正則等工具先過濾引數,也可以解決這個sql注入問題。

  • 如果對mysql比較有研究的,會知道一條sql的執行過程是怎麼樣的?首先當客戶端向mysql伺服器傳送一條sql的時候,會先經過查詢快取,如果是之前查過的,則直接返回結果;否則,sql會被解析成mysql一種特有的資料結構,叫做解析樹,然後再通過優化器優化,最後呼叫儲存引擎api執行sql;如果一條比較複雜的sql,經過解析是會消耗比較多資源和時間,那麼問題來了?如果一條語句,只是其中的幾個引數不一樣或者在併發執行的時候,如果不採用預處理語句,則需要反覆進行解析,給mysql帶來資源消耗,同時加重web伺服器的負載。

錯誤處理模式

pdo 提供了三種不同的錯誤處理模式,以滿足不同風格的應用開發,注意點:不管當前是否設定了 PDO::ATTR_ERRMODE ,如果連線失敗,PDO::__construct() 將總是丟擲一個 PDOException 異常。未捕獲異常是致命的。

  1. PDO::ERRMODE_SILENT
    此為預設模式。 pdo 將只簡單地設定錯誤碼,可使用 PDO::errorCode() 和 PDO::errorInfo() 方法來檢查語句和資料庫物件
    $dsn = 'mysql:host=172.17.0.4;dbname=test';
    $user = 'root';
    $pass = 'root';
    try {    
    $dbh = new PDO($dsn, $user, $pass);
    } catch (PDOException $e) {    
    echo 'Connect error:'. $e->getMessage();    
    die;
    }
    $dbh->query('select * from `test`');
    print_r($dbh->errorCode());
    echo '<br/>';
    print_r($dbh->errorInfo());

    PHP 資料庫擴充之 PDO

  2. PDO::ERRMODE_WARNING
    除設定錯誤碼之外,PDO 還將發出一條傳統的 E_WARNING 資訊。如果只是想看看發生了什麼問題且不中斷應用程式的流程,那麼此設定在除錯/測試期間非常有用。
    $dsn = 'mysql:host=172.17.0.4;dbname=test';
    $user = 'root';
    $pass = 'root';
    try {    
    $dbh = new PDO($dsn, $user, $pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_WARNING]);
    } catch (PDOException $e) {    
    echo 'Connect error:'. $e->getMessage();    
    die;
    }
    $dbh->query('select * from `test`');
    print_r($dbh->errorCode());
    echo '<br/>';
    print_r($dbh->errorInfo());

    PHP 資料庫擴充之 PDO

  3. PDO::ERRMODE_EXCEPTION
    除設定錯誤碼之外,PDO 還將丟擲一個 PDOException 異常類並設定它的屬性來反射錯誤碼和錯誤資訊。此設定在除錯期間也非常有用,因為它會有效地放大指令碼中產生錯誤的點,從而可以非常快速地指出程式碼中有問題的潛在區域(記住:如果異常導致指令碼終止,則事務被自動回滾)。
    異常模式另一個非常有用的是,相比傳統 PHP 風格的警告,可以更清晰地構建自己的錯誤處理,而且比起靜默模式和顯式地檢查每種資料庫呼叫的返回值,異常模式需要的程式碼/巢狀更少。
    $dsn = 'mysql:host=172.17.0.4;dbname=test';
    $user = 'root';
    $pass = 'root';
    try {    
    $dbh = new PDO($dsn, $user, $pass);
    } catch (PDOException $e) {    
    echo 'Connect error:'. $e->getMessage();    
    die;
    }
    $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $dbh->query('select * from `test`');

    PHP 資料庫擴充之 PDO

    使用PDO封裝mysql

github

地址:https://github.com/ZengJiangBin/Mysql
程式碼會慢慢更新總結,也希望大神們可以給出更多的意見和留言

相關文章