2016年10月13日 星期四

PDO 入門介紹與應用

        很多使用php寫的舊系統,連結mysql用的方式為mysql_connect()、mysql_xxxxxx.....,但php從5.5.0就不再對mysql_的擴充提供支援了,雖然還能在之後像5.6.x的版本使用,但是速度及極可能發生sql injection的低安全性是mysql_擴充應該被淘汰的主因,而且以現在的趨勢,php7會成為之後的開發主流,而mysql_擴充則是完完全全在php7被拿掉!

        於是一部分人轉用了mysqli_擴充,因轉換的速度非常快,不用在原有的架構做太多的變動,基本上除了一些呼叫的變數不一樣位置以外,幾乎加個「i」再微調幾下就好了,mysqli的確比mysql速度還要快,安全性也要更高,但兩者共同有一個缺點:如果專案需要使用不同的資料庫(Ex. mysql、sqlite3、......),那就必須要引入並使用不同的資料庫擴充語法和介面,想想就蠻麻煩的吧。

        所以進到了今天的重頭戲:PDO,PDO是一個抽象層(abstract layer),這個抽象層就建立在我們寫的php和php連接到資料庫的方式之間,不過雖然PDO支援不同的資料庫之間(上面提及的( mysql 、sqlite3 ),但送入執行的sql語法一樣是我們自己寫的,意思是如果有些sql語法是特定資料庫才提供的特性,那麼嚴格來說PDO還是沒有完美的提供一個統整的解決方案,以現階段來說,在專案裡面還是需要調整sql語法使其支援所有使用的資料庫。

        以下來示範實作:

try {
    $host = '127.0.0.1';
    $dbname = 'dbname';
    $username = 'username';
    $password = 'password';
    $dsn = 'mysql:host=' . $host . ';dbname=' . $dbname;
    $pdo = new PDO($dsn, $username, $password);
} catch (PDOException $e) {
    echo "錯誤訊息:" . $e->getMessage(), PHP_EOL;
    exit;
}

        建立好了連結,接著建好資料:


        最簡單取值的方式:
     
$sql = " select * from test";
$result_query = $pdo->query($sql);
$result_fetch = $result_query->fetchAll();

echo $result_fetch[0]['first_name'], PHP_EOL;

        結果:Chih, Hong

        此外,PDO擴充還提供了一個很方便的「消毒」方式,即使前端已經過濾了使用者輸入,但是關係到資料庫的安全,後端接到前端丟過來的值時,還是一樣要做消毒,以下是PDO的消毒實作:

$sql = ' select * from test where last_name = :last_name';
$statement = $pdo->prepare($sql);
// 假設用POST接到值是'Chen'$post_lastname = 'Chen';
$statement->bindValue(':last_name', $post_lastname);
$statement->execute();

$result_fetch = $statement->fetchAll();
echo $result_fetch[0]['first_name'], PHP_EOL;
     
        先在原本的sql條件加了個佔位符,也就是:last_name,接著prepare之後,就能安全地在用bindValue去綁定任何值。

        結果:Chih, Hong

        bindValue()其實有三個參數,第三個參數如果沒設定的話,PDO會假定資料為String,而如果現在要查詢的條件為整數、布林值或NULL,那我們必須要改變一點寫法:

$sql = ' select * from test where id = :id';
$statement = $pdo->prepare($sql);
// 假設用POST接到值是1
$post_id = 1;
$statement->bindValue(':id', $post_id, PDO::PARAM_INT);
$statement->execute();

$result_fetch = $statement->fetchAll();
echo $result_fetch[0]['first_name'], PHP_EOL;

        結果:Chih, Hong

        這裡有個細節要小心,bindValue裡的佔位符變數和sql語法裡的佔位變數大小寫不一樣的話,是不會通過的,PDO會拋出Exception告訴你佔位符尚有變數未設定,意思是說,如果sql裡寫的是:id,bindValue設定的是:Id,就會出錯,一定要特別注意。

        出錯訊息:


     
        接著介紹最後一個PDO也很容易實現的特點:交易機制

        專案上線之前,勢必要把天災人禍考慮進去,萬一機房進水、虛擬主機商跑路或是停電種種狀況發生,當sql進到一半,可能一連串資料有的進了資料庫,有的卻沒進去,結果最後資料對不起來,整個一團亂,所以需要一個機制去控制資料,讓他們同時成功,或是同時失敗,范逸臣突然拍我肩膀說,講這麼多幹嘛,總之就一句話:

        「 留下來,或是我跟你走!」

        好的,接下來實作交易機制:

        首先把資料庫調整一下


        假設我跟阿范(突然很熟?!)都各自有100,某天我夾自助餐的時候,不小心夾了120,糟糕,趕快跟阿范借20,所以這裡我們兩筆資料同時要做變動,我將會多20,而阿范會少20,阿范轉給我20的時候,如果系統有突發狀況,我沒收到20,他卻已經扣了20,就會非常的麻煩,還好系統有交易機制:

$sql1 = ' update test set money = money - :money where last_name = :last_name';
$sql2 = ' update test set money = money + :money where last_name = :last_name';

$statement1 = $pdo->prepare($sql1);
$statement2 = $pdo->prepare($sql2);

// 開始交易
$pdo->beginTransaction();

// 假設匯款的是 Fan
$from_lastname = 'Fan';
$withdrawal = 20;
$statement1->bindParam(':money', $withdrawal, PDO::PARAM_INT);
$statement1->bindParam(':last_name', $from_lastname, PDO::PARAM_STR);
$statement1->execute();

// 假設收款的是 我
$to_lastname = 'Chen';
$deposit = 20;
$statement2->bindParam(':money', $deposit, PDO::PARAM_INT);
$statement2->bindParam(':last_name', $to_lastname, PDO::PARAM_STR);
$statement2->execute();

// 承認交易
$pdo->commit();

        系統使用了beginTransaction及commit,將這組動作包裝成單一的資料庫交易,確定這兩筆會同時成功,或是同時失敗。

        結果:


        終於可以吃便當了!


參考資料:

http://php.net/manual/en/intro.mysql.php
http://php.net/manual/en/intro.mysqli.php
http://stackoverflow.com/questions/23134769/pdostatementexecute-sqlstatehy093-invalid-parameter-number-parameter-wa
https://www.tenlong.com.tw/items/9863477788?item_id=1007177
http://www.books.com.tw/products/0010556526


沒有留言:

張貼留言