PDOを用いたPHPのデータベース接続・操作

現在、PHPでデータベースに接続する際にはPDOを使う方法が主流だそうです（ドットインストールより）。そこで、PDOの基本的な使い方を確認していこうと思います。

# データベースへの接続方法
基本的なフォーマットは次のようになります。

```php:db_connect.php
<?php

define('DB_USERNAME', 'myname');
define('DB_PASSWORD', '12345678');
define('DSN', 'mysql:host=localhost; dbname=testdb; charset=utf8');

function db_connect(){
    $dbh = new PDO(DSN, DB_USERNAME, DB_PASSWORD);
    return $dbh;
}

?>
```

```php:⚪︎⚪︎⚪︎.php
<?php

...

require_once('db_connect.php');

...

try {
    // データベースに接続
    $dbh = db_connect();

    //例外処理を投げるようにする（throw）
    $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    // データベースから値を取ってきたり、 データを挿入したりする処理 
    $statement = $dbh->query('SELECT * FROM users');      // 例
  
    //データベース接続切断
    $statement = null;
    $dbh = null;

} catch (PDOException $e) {
    header('Content-Type: text/plain; charset=UTF-8', true, 500);
    // エラー内容は本番環境ではログファイルに記録して， Webブラウザには出さないほうが望ましい
    exit($e->getMessage()); 
}

...

?>

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Title</title>
    </head>
    <body>
        <!-- DBに関する処理はここでは行わないこと -->
    </body>
</html>
```


db_connect.phpの
`$dbh = new PDO(DSN, DB_USERNAME, DB_PASSWORD);`
によりデータベースへの接続が行われます。

## DSN 
データベースに接続するために必要な情報になります。MySQLの基本的な書き方を例に挙げると、
`mysql:dbname=test;host=localhost;charset=utf8mb4`
というようになります。dbnameにはデータベース名を、hostにはホスト名またはIPアドレスを、charsetには文字セットを入力します。
: と ; の使い分けに注意しましょう。

## DB_USERNAME
データベースのユーザー名です。

## DB_PASSWORD
データベースに接続するためのパスワードです。

また、⚪︎⚪︎⚪︎.phpの
`$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);`
についてですが、このコードにより、SQL実行でエラーが起こった際に例外をスローしてくれるようになります。決まり文句だと思っても良さそうです。

# SQLの実行方法（３パターン）

## [PDO::exec](http://www.php.net/manual/ja/pdo.exec.php) 
結果を返す必要がなく、安全な（ユーザー入力を伴わない）SQLを実行する際に用います。

```php
$dbh->exec('INSERT INTO users (name, age) values ('taguchi', 55)');
```

## [PDO::query](http://www.php.net/manual/ja/pdo.query.php) 
安全な（ユーザー入力を伴わない）SQLで、返り値として`PDOStatementオブジェクト`が必要な場合に使います。

```php
$statement = $dbh-> query('SELECT * FROM users');
```

## [PDO::prepare](http://www.php.net/manual/ja/pdo.prepare.php) → [PDOStatement::bindValue](http://www.php.net/manual/ja/pdostatement.bindvalue.php) → [PDOStatement::execute](http://www.php.net/manual/ja/pdostatement.execute.php)

安全対策が必要な（ユーザー入力を伴う）SQLを実行する際に用いる方法です。返り値として`PDOStatementオブジェクト`が得られます。

ユーザー入力を受け取ってSQL文を動的に生成する場合は、`プリペアドステートメント`と`プレースホルダ`を用いて、SQLインジェクションの対策を行う必要があります。この際、`PDO::ATTR_EMULATE_PREPARES`は`false`にしておきましょう。

```php
//ユーザが入力した値
$name = $_SESSION['name'];
$age = $_SESSION['age'];
$password_hash =  password_hash($_SESSION['password'], PASSWORD_DEFAULT);

//静的プレースホルダを用いるためにエミュレーションを無効化
$dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

// プレースホルダを用いたSQL文を生成（プリペアドステートメント）
$statement = $dbh->prepare("INSERT INTO users (name, age, password, date) VALUES (:name, :age, :password_hash, now())");
// プレースホルダへ実際の値を設定する
$statement->bindValue(':name', $name, PDO::PARAM_STR);
$statement->bindValue(':age', $age, PDO::PARAM_INT);
$statement->bindValue(':password_hash', $password_hash, PDO::PARAM_STR);
// プリペアドステートメントを実行
$statement->execute();
```

# SELECT文による結果の取得
まず、以下のようなSELECT文を実行したとします。

```php
$statement = $dbh-> query('SELECT * FROM users');
```

## [PDOStatement::fetch](http://www.php.net/manual/ja/pdostatement.fetch.php) 
結果セットから１行ずつ、次の行を取得してくれます。全ての取得が終わると`false`を返します。

```php
while ($user = $statement->fetch()) {
    printf("%s is %d years old.\n", $user['name'], $user['age']);
}
```

## [PDOStatement::fetchAll](http://www.php.net/manual/ja/pdostatement.fetchall.php)
結果セットを全件取得して、2次元配列にします。 

```php
$users = $statement->fetchAll();
for($i = 0; $i < count($users); $i++) {
    printf("%s is %d years old.\n", $user[$i]['name'], $user[$i]['age']);
}
```

# 行数の取得
##DELETE, INSERT, UPDATE文 （ [PDOStatement::rowCount] (http://www.php.net/manual/ja/pdostatement.rowcount.php) ）

PDOStatement::rowCountを用いると、直近のSQLステートメント（DELETE, INSERT, UPDATE文）によって作用した行数を返してくれます。SELECT文で用いるのは推奨されていないようです。

```php
// Usersテーブルから全ての行を削除する 
$statement = $dbh->query('DELETE FROM users');

// 削除された行数を返す 
$count = $statement->rowCount();
print("Deleted %d rows.\n", $count);
```

## SELECT文

推奨されるのは以下の２通りです。

- PDOStatement::fetchAll メソッドで結果全件を配列として取得し、それに対してPHPの [count](http://www.php.net/manual/ja/function.count.php) 関数を使う。

```php
$statement = $dbh->query('SELECT * FROM users');
// 結果セットを全件取得
$users = $statement->fetchAll();

// 件数をカウント
$count = count($users);
```

- `SELECT COUNT(*) WHERE ... ` といったクエリを発行し、その結果を [PDOStatement::fetchColumn](http://www.php.net/manual/ja/pdostatement.fetchcolumn.php) メソッドで得る。

```php
$statement = $dbh->query("SELECT COUNT(*) FROM users WHERE age > 20");
$count = $statement->fetchColumn();
```

# トランザクション
ある一連の処理が必ず行われることを保証してくれるための仕組みです。基本的には以下のような形で使用します。

```php
define('DB_USERNAME', 'myname');
define('DB_PASSWORD', '12345678');
define('DSN', 'mysql:host=localhost; dbname=testdb; charset=utf8');

try {
  // DB接続
  $db = new PDO(PDO_DSN, DB_USERNAME, DB_PASSWORD);
  // 例外処理をスロー
  $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

  // トランザクション開始
  $db->beginTransaction();
  // 一連で行いたい処理
  $db->exec("update users set score = score - 10 where name = 'taguchi'");
  $db->exec("update users set score = score + 10 where name = 'suzuki'");
  // トランザクションをコミット
  $db->commit();

} catch (PDOException $e) {
  // トランザクションをロールバック
  $db->rollback();
  echo $e->getMessage();
  exit;
}
```

トランザクション処理において用いるメソッドは、主に以下の３つです。

- [PDO::beginTransaction](http://www.php.net/manual/ja/pdo.begintransaction.php)
  - トランザクションを開始する
- [PDO::commit](http://www.php.net/manual/ja/pdo.commit.php)
  - この時点（一連の処理が全てうまくいった時点）で、一連の処理を初めて結果に反映させる
- [PDO::rollBack](http://www.php.net/manual/ja/pdo.rollback.php)
  - 一連の処理のどこかで失敗した場合に、データを元に戻してトランザクション開始前の状態にする 

以上でこの記事を終了します。
間違い等ございましたら、コメントで教えていただけると幸いです。