MDB2→PDOに移行する

MDB2→PDO移行の覚えがき。
DB接続、トランザクション、ステートメント実行といった基本的な内容が中心。

## 公式マニュアル

公式マニュアルのTOPページはそれぞれ以下の通り。

* MDB2
    * [MDB2 | pear.php.net](https://pear.php.net/manual/en/package.database.mdb2.php)

* PDO
    * [PHP Data Objects | php.net](https://www.php.net/manual/ja/book.pdo.php)

## 1. DB接続

|  |MDB2  |PDO  |
|---|---|---|
|接続  | MDB2::factory, MDB2::connent, MDB2::singleton | new PDO |
|接続エラー  | PEAR::isError | PDOExceptionをスロー |
|切断  | MDB2::disconnent |nullをセット |

### (1) 接続
いろいろな書き方があるので、とりあえず代表的そうなものを抜粋。

```php:MDB2
/* MySQL */
$dbh = MDB2::connect("mysql://$user:$password@$host:$port/$dbname");
/* PostgreSQL */
$dbh = MDB2::connect("pgsql://$user:$password@$host:$port/$dbname");
/* Oracle */
$dbh = MDB2::connect("oci8://$user:$password@$host:$port/?service=$dbname");
```
```php:PDO
/* MySQL */
$dbh = new PDO("mysql:host=$host;port=$port;dbname=$dbname", $user, $password);
/* PostgreSQL */
$dbh = new PDO("pgsql:host=$host;port=$port;dbname=$dbname;user=$user;password=$password");
/* Oracle */
$dbh = new PDO("oci:dbname=".$dbinfo, $user, $password);
```

### (2) 接続エラー
```php:MDB2
$dbh = MDB2::connect($connection_string);
if (PEAR::isError($dbh)) {
    // 接続に失敗した場合の処理を書く
}
```

```php:PDO
try {
    $dbh = new PDO($connection_string);
} catch (PDOException $e) {
    // 接続に失敗した場合の処理を書く
}
```

### (3) 切断

```php:MDB2
$dbh->disconnect();
```
```php:PDO
$dbh = null;
```

### 参考

* MDB2 
    * [Connecting and disconnecting a database | pear.php.net](https://pear.php.net/manual/en/package.database.mdb2.intro-connect.php)
    * [The Data Source Name | pear.php.net](https://pear.php.net/manual/en/package.database.mdb2.intro-dsn.php)

* PDO
    * [接続、および接続の管理 | php.net](https://www.php.net/manual/ja/pdo.connections.php)
    * [MySQL 関数 (PDO_MYSQL)  | php.net](https://www.php.net/manual/ja/ref.pdo-mysql.php)
    * [PDO_PGSQL DSN | php.net](https://www.php.net/manual/ja/ref.pdo-pgsql.connection.php)
    * [Oracle 関数 (PDO_OCI) | php.net](https://www.php.net/manual/ja/ref.pdo-oci.php)
    
## 2. トランザクション

内容的にほとんど同じ。
トランザクションサポートの有無の確認の仕方が違うくらい。

|  |MDB2  |PDO  |
|---|---|---|
|開始  | MDB2::beginTransaction | PDO::beginTransaction |
|サポートの確認（※）  | MDB2::support('transactions') |非対応の場合、PDOExceptionをスロー |
|コミット  | MDB2::commit |PDO::commit|
|ロールバック  | MDB2::rollBack |PDO::rollBack|

※: サポート確認: 「トランザクションサポートの有無の確認」を指しています。

### (1) 開始, (2) サポートの確認

```php:MDB2
$dbh->beginTransaction();
if(!$dbh->supports('transactions')){
    // トランザクションサポートがない場合の処理を記述
}
```
```php:PDO
try {
    $dbh->beginTransaction();
} catch (PDOException $e) {
    // トランザクションサポートがない場合の処理を記述
}
```

### (3) コミット

```php:MDB2
$dbh->commit();
```
```php:PDO
$dbh->commit();
```

### (4) ロールバック

```php:MDB2
$dbh->rollback();
```
```php:PDO
$dbh->rollback();
```

### 参考

* MDB2
    * [Performing transactions | pear.php.net](https://pear.php.net/manual/en/package.database.mdb2.intro-transaction.php)

* PDO
    * [トランザクションおよび自動コミット | php.net](https://www.php.net/manual/ja/pdo.transactions.php)
    * [PDO::beginTransaction | php.net](https://www.php.net/manual/ja/pdo.begintransaction.php)
    * [PDO::commit | php.net](https://www.php.net/manual/ja/pdo.commit.php)
    * [PDO::rollBack](https://www.php.net/manual/ja/pdo.rollback.php)

## 3. ステートメントの準備と実行

MDB2→PDOの移行で最もエネルギーを使いそうなところ。
ステートメントを実行した後のデータの取り出し方も異なっているので、そこも注意すべきポイントだったり。

|  |MDB2  |PDO  |
|---|---|---|
| 値のバインド |MDB2::quote | PDOStatement::bindParam, PDOStatement::bindValue | 
| 値のデータ型（SQL INTEGER） |integer | PDO::PARAM_INT | 
| 値のデータ型（SQL CHAR, VARCHAR, 他文字列データ型） |text | PDO::PARAM_STR | 
| 値のデータ型（FLOAT型） |float | PDO::PARAM_STR もしくは PDO::PARAM_INT| 
| 値のデータ型（ブールデータ型） |boolean| PDO::PARAM_BOOL | 
| 値のデータ型（タイムスタンプ型） |timestamp| PDO::PARAM_STR（※2） | 
| 値のデータ型（日付型） |date| PDO::PARAM_STR（※） | 
| 実行に失敗した場合 |MDB2 Errorオブジェクトを返す| falseを返す<br>※PDO::ATTR_ERRMODEにPDO::ERRMODE_EXCEPTIONをセットすると、PDOExceptionとなる | 

※1: FLOAT型に関し、PDO::PARAM_INTを使う場合は、PDO::ATTR_EMULATE_PREPARESをtrueにすること
※2: PDOには、タイムスタンプ型、日付型が存在しないので、代わりに文字列型のPDO::PARAM_STRを使用する

### (1)  変数や値のバインドを伴うプリペアドステートメントの実行

```php:MDB2
$stmt = 'INSERT INTO tablename (id, itemname, saved_time) VALUES ('
    . $dbh->quote($id,   'integer')   .', '
    . $dbh->quote($name, 'text')      .', '
    . $dbh->quote($time, 'timestamp') .')';
$res = $dbh->exec($stmt);
```

```php:PDO
$stmt = 'INSERT INTO tablename (id, itemname, saved_time) VALUES (:id, :name, :saved_time)';
$sth = $dbh->prepare($stmt);
$sth->bindValue(':id', $id, PDO::PARAM_INT);
$sth->bindValue(':name', $name, PDO::PARAM_STR);
$sth->bindValue(':saved_time', $saved_time, PDO::PARAM_STR);
$res = $sth->execute();
```

### (2) 入力値の配列を伴うプリペアドステートメントの実行(名前つきパラメータ)
```php:MDB2
$sth = $dbh->prepare('SELECT name, colour, calories FROM fruit
    WHERE calories < :calories AND colour = :colour');
$res = $sth->execute(array(':calories' => $calories, ':colour' => $colour));
while ($row = $res->fetchRow()) {
    // 取り出す内容を記述
}
```

```php:PDO
$calories = 150;
$colour = 'red';
$sth = $dbh->prepare('SELECT name, colour, calories FROM fruit
    WHERE calories < :calories AND colour = :colour');
$sth->execute(array(':calories' => $calories, ':colour' => $colour));

foreach ($sth->fetchAll() as $row) {
    // 取り出す内容を記述
}
```

### (3) 入力値の配列を伴うプリペアドステートメントの実行 (プレースホルダ)
```php:MDB2
$calories = 150;
$colour = 'red';
$sth = $dbh->prepare('SELECT name, colour, calories FROM fruit
    WHERE calories < ? AND colour = ?');
$res = $sth->execute(array($calories, $colour));
while ($row = $res->fetchRow()) {
    // 取り出す内容を記述
}
```

```php:PDO
$calories = 150;
$colour = 'red';
$sth = $dbh->prepare('SELECT name, colour, calories FROM fruit
    WHERE calories < ? AND colour = ?');
$sth->execute(array($calories, $colour));
foreach ($sth->fetchAll() as $row) {
    // 取り出す内容を記述
}
```

### (4) 多数の値のバインドを伴う＆データ型の指定が必要な場合

```php:MDB2
$types = array('integer', 'text', 'text', 'date', 'date');
$sth = $dbh->prepare('INSERT INTO numbers VALUES (?, ?, ?, ?, ?)', $types, MDB2_PREPARE_MANIP);
$params = array(1, 'one', 'en', date("Y-m-d H:i:s"), date("Y-m-d H:i:s"));
$res = $sth->execute($params);
while ($row = $res->fetchRow()) {
    // 取り出す内容を記述
}
```

MDB2だとprepareの第2引き数にデータ型を詰めた配列をセットしてあげればいいけれど、PDOだとそうはいかない。bindParamやbindValueを何回も書くのもな、という場合は関数化してしまえばOK。
また、こうすれば、パラメータの数が可変の場合も対応できる。

```php:PDO
$sth = $dbh->prepare('INSERT INTO numbers VALUES (?, ?, ?, ?, ?)');
$params = array(1, 'one', 'en', date("Y-m-d H:i:s"), date("Y-m-d H:i:s"));
$types = array('integer', 'text', 'text', 'date', 'date');
$sth = bindValues($stmt, $params, $types);
$sth->execute();
foreach ($sth->fetchAll() as $row) {
    // 取り出す内容を記述
}

function bindValues($sth, $params, $types){
    $i = 0;
    while($i < count($params)){
        switch($types[$i]){
            case 'boolean':
                $bindType = PDO::PARAM_BOOL;
                break;
            case 'integer':
                $bindType = PDO::PARAM_INT;
                break;
            case ('text' || 'timestamp' || 'date'):
                $bindType = PDO::PARAM_STR;
                break;
            default:
                return false;
        }
        $sth->bindValue($i+1, $params[$i], $bindType);
        $i++;
    }
    return $sth;
}
```
※1: 上記はあくまで例なので、使用しているDBにおけるデータ型を確認して、適切な形に書き換えるようにしてください。
※2: おかしな値が指定された時用にfalseを返してますが、上記コード中ではその際の処理は書いてません。

### (5) 実行に失敗した場合

MDB2では、MDB2 Errorオブジェクトを返すのに対し、PDOではデフォルトでは、falseを返します。

そのままだと、try catch文では、失敗を検知できませんが、PDO::ATTR_ERRMODEにPDO::ERRMODE_EXCEPTIONをセットすると、PDOExceptionとすることができます。
書き換えをラク＆処理を煩雑化させないためにも、後者としておくのが良いかと思います。
DB接続時（というより直後?）に、設定しておきましょう。
その他、DB処理をしている間は、try catch文内に入れておくのがよさそうです。



```php:MDB2
$res = $sth->execute();
if (PEAR::isError($res)) {
    // 実行に失敗した場合の処理を記述
}
```

```php:PDO
/* デフォルトの場合 */
$res = $sth->execute();
if (!res) {
    // 実行に失敗した場合の処理を記述
}

/* 以下の記述により、PDOExceptionにできる */
try {
    // DB接続処理
    $dbh = new PDO($connection_string);
    $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    /* ・・　省略 ・・*/
    $res = $sth->execute();
} catch (PDOException $e) {
    // 実行に失敗した場合の処理を書く
}
```



### 参考

* MDB2
    * [Quote values in a suitable format to compose a query | pear.php.net](https://pear.php.net/manual/en/package.database.mdb2.intro-quote.php)
    * [Prepare and execute SQL statements | pear.php.net](https://pear.php.net/manual/en/package.database.mdb2.intro-execute.php)
    * [Querying | pear.php.net](https://pear.php.net/manual/en/package.database.mdb2.intro-query.php)
* PDO
    * [PDO::prepare | php.net](https://www.php.net/manual/ja/pdo.prepare.php)
    * [PDOStatement::execute | php.net](https://www.php.net/manual/ja/pdostatement.execute.php)
    * [PDO::exec | php.net](https://www.php.net/manual/ja/pdo.exec.php)
    * [定義済み定数 | php.net](https://www.php.net/manual/ja/pdo.constants.php)
    * [PDOException クラス | php.net](https://www.php.net/manual/ja/class.pdoexception.php)
    * [PDO::query | php.net](https://www.php.net/manual/ja/pdo.query.php)
    * [あの PDO::PARAM_INT の挙動が変わった！ | Qiita](https://qiita.com/te2ji/items/a8e7211a69f313126f7c)

## 4. フェッチモード

それぞれ接続時、SQL実行前など、いろいろなタイミングで設定が可能。
PDOはステートメントに対して指定することもできる。

|  |キーor プロパティ|MDB2  |PDO  |
|---|---|---|---|
|規則配列 | キー: カラム番号 | MDB2_FETCHMODE_ORDERED |PDO::FETCH_NUM|
|連想配列 | キー: カラム名| MDB2_FETCHMODE_ASSOC |PDO::FETCH_ASSOC|
|オブジェクト | プロパティ：カラム名 |MDB2_FETCHMODE_OBJECT|PDO::FETCH_OBJ |
|デフォルト | -（デフォルトの設定値） | MDB2_FETCHMODE_ORDERED |PDO::FETCH_BOTH|

### (1) 接続時に指定
```php:MDB2
$dbh = MDB2::connect($connection_string);
$dbh->setFetchMode(MDB2_FETCHMODE_ASSOC);
```
```php:PDO
$dbh = new PDO($connection_string);
$dbh->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
```

### (2) 実行時に指定
```php:MDB2
$dbh->setFetchMode(MDB2_FETCHMODE_ASSOC);
$sth = $dbh->prepare("SELECT aaa, bbb FROM hoge");
$res = $sth->execute();
while ($row = $res->fetchRow()) {
    // 取り出す内容を記述
}
```
```php:PDO
$sth = $dbh->prepare("SELECT aaa, bbb FROM hoge");
// ステートメントに対し、設定
$sth->setFetchMode(PDO::FETCH_ASSOC);
$sth->execute();
foreach ($sth->fetchAll() as $row) {
    // 取り出す内容を記述
}
```

### (3) フェッチ時に指定
```php:MDB2
$sth = $dbh->prepare("SELECT aaa, bbb FROM hoge");
$res = $sth->execute();
while ($row = $res->fetchRow(MDB2_FETCHMODE_ASSOC)) {
    // 取り出す内容を記述
}
```
```php:PDO
$sth = $dbh->prepare("SELECT aaa, bbb FROM hoge");
$sth->execute();

foreach ($sth->fetchAll(PDO::FETCH_ASSOC) as $row) {
    print $row;
}
```



### (4) 結果セット

#### ①規則配列
MDB2でMDB2_FETCHMODE_ORDERED, PDOでPDO::FETCH_NUMを指定した場合の結果セット

```
Array
(
    [0] => Taro
    [1] => 12
    [2] => male
)
```

#### ②連想配列
MDB2でMDB2_FETCHMODE_ASSOC, PDOでPDO::FETCH_ASSOCを指定した場合の結果セット

```
Array
(
    [name] => Taro
    [age] => 12
    [sex] => male
)
```

#### ③オブジェクト
MDB2でMDB2_FETCHMODE_OBJECT, PDOでPDO::FETCH_OBJを指定した場合の結果セット

```
stdClass Object
(
    [name] => Taro
    [age] => 12
    [sex] => male
)
```

### 参考

* MDB2
    * [Obtaining data from query results | pear.php.net](https://pear.php.net/manual/en/package.database.mdb2.intro-fetch.php)

* PDO
    * [PDO::setAttribute | php.net](http://phpweb.hostnet.com.br/manual/ja/pdo.setattribute.php)
    * [PDOStatement::setFetchMode | php.net](https://www.php.net/manual/ja/pdostatement.setfetchmode.php)
    * [PDOStatement::fetch | php.net](https://www.php.net/manual/ja/pdostatement.fetch.php)
    * [PDO::FETCH_NUM 配列を使用 | dozo PukiWiki](http://dozo.matrix.jp/pear/index.php/PECL/pdo/fetch/FETCH_NUM.html)

## 終わりに

MDB2は、

* 最新のリリースが2012年10月とかなり前（詳しくは[コチラ](https://pear.php.net/package/MDB2)）
* 使用に必要なPEARがPHP7.4からデフォルトでは使えず非推奨になり、将来的に削除される見通し（詳しくは[コチラ](https://www.php.net/manual/ja/migration74.other-changes.php))

ことから、早めにPHPの標準クラスであるPDOに移行していきたいところ。

## 変更履歴

* 2021/07/29: 実行失敗時の書き換えを追加（3-(5)）、記述ミスの修正（3のMDB2のforeachあたり）
* 2021/07/30: フェッチモードにデフォルトのモードを追記（4）、フェッチモード指定のタイミングを追記（4-(3)）
* 2021/08/20: MDB2のフェッチモードの指定に誤りがあったため、修正（4-(1), (2))
* 2021/11/25: PDOのSQL実行後のデータの取り出し方に誤りがあったため、修正（3-(3), (4))
* 2022/07/15: PDOのプリペアドステートメントの実行の仕方に誤りがあったため、修正（3-(1)）
* 2022/07/15: PDOのSQL実行後のデータの取り出し方に誤りがあったため、修正（3-(2), 4-(2)）
* 2022/09/08: ステートメントの準備と実行に関し、FLOAT型についての言及を追記（3）