PDO_pgsqlでハマった件 (PHP Advent Calendar 2010 17日目)

12/17 が終了して数時間が経ちましたが、16日目「匿名関数と無名関数 (PHP Advent Calendar 2010 16日目) | dogmap.jp」の wokamoto さんからバトンを渡されたPHP Advent Calendarの 17日目です。


さて PHP 5.2.X も終了が宣言された昨今、みなさんの PHP-5.3 への移行は順調に進んでいますでしょうか? 私も自分の環境やメンテナンス対象のコードを徐々に 5.3 対応に修正しているところです。今回はその中で DB アクセスライブラリを Creole から PDO に変更した際にハマった事を書こうと思います。


最近はフレームワークを使ったプログラミングが一般的なので、DBアクセスもフレームワークの Model クラスや、フレームワークがサポートしている Doctrine などの O/R マッパーを使うのが普通でしょうね。私は独自のアクセスライブラリを自前で作ってまして、フレームワークを使うまでもないプログラムにはそれを使っています。夏に開催された PHP Matsuri に参加して作成・公開した Keires_DB は、その独自ライブラリから他への依存部分を外して単独で使用できるようにしたものです。以前は DB 抽象化レイヤに Creole を使ってたのですが、Creole も開発が終了してからずいぶん経ちますし、5.3 に移行するこの機会に PDO に置き換えてみました。

Keires_DBの使い方を簡単に書いておくとこんな感じです。

  CREATE TABLE hogetbl                                                         
      col1    INTEGER PRIMARY KEY,                                              
      col2    TEXT,                                                             
      col3    TIMESTAMP,                                                        
      col4    BOOLEAN                                                           
  );   

というテーブルがあったとして、こんなクラスを作ります。

<?php
require_onde 'Keires/DB.php';
class DB_hoge extends Keires_DB_Abstract {
    static public $table_name = 'hogetbl';
    static public $tupple_info = array(
        'col1' => array(
            'type' => 'integer',
            'primary' => true,
            ),
        'col2' => array(
            'type' => 'text',
            ),
        'col3' => array(
            'type' => 'timestamp',
            ),
        'col4' => array(
            'type' => 'boolean',
            ),
        );
}

あとは初期化をしてアクセス。

<?php
$options = array(
    'types' => array(
        'default' => array(
            'dsn' => 'pgsql:host=localhost;user=username;dbname=dbname',
            ),
        ),
    );
Keires_DB::init($options);
// 書き込み
$data = array(
    'col1' => 1,
    'col2' => 'fugahoge',
    'col3' => date('c'),
    'col4' => false,
    );
DB_hoge::store($data);
// 読み込み
$col1 = 1;
$val = DB_hoge::getInfo($col1);
// 削除
DB_hoge::delete($col1);

さて上手く動いていたと思われていた Keires_DB ですが、最近不具合を発見しました。PostgreSQL との組み合わせで BOOLEAN なカラムに FALSE を保存しようとすると

"ERROR:  invalid input syntax for type boolean: """

というエラーがサーバから返るのです。実行されたSQLを調べると PDO::prepare()には想定通りのSQLが、PDOStatement::execute()にもちゃんと FALSE 値が渡ってました。該当部分の一部はこんな感じです。

<?php
    // $sql には ? プレースホルダ形式のSQL
    // $params にはバインドされる値が入ってます
     public function execute($sql, $params = array()) {
         $stmt = $this->_db->prepare($sql);
         return $stmt->execute($params);
     }

なぜこれで BOOLEAN で FALSE 値の場合のみ問題になるのか、ずいぶん悩んだんですが、マニュアルを熟読していてはたと気づきました。

実行される SQL 文の中のバインドパラメータと同数の要素からなる、 値の配列。すべての値は PDO::PARAM_STR として扱われます。

http://jp2.php.net/manual/ja/pdostatement.execute.php

PDOStatement::execute()に値を配列で渡す場合は、すべての値が文字列として解釈されるので FALSE 値は空文字列に変換されていたのです。


そこでexecute()メソッドに一気に値を渡すのを止めて、値を一つずつbindするメソッドを作りました。

<?php
    protected function _execStatement($stmt, $params) {
        $idx = 1;
        foreach ($params as $param) {
            $btype = PDO::PARAM_STR;
            if (is_int($param)) {
                $btype = PDO::PARAM_INT;
            } else if (is_bool($param)) {
                $btype = PDO::PARAM_BOOL;
            } else if (is_null($param)) {
                $btype = PDO::PARAM_NULL;
            }
            $bresult = $stmt->bindValue($idx, $param, $btype);
            if (!$bresult) {
                $errinfo = $stmt->errorInfo();
                throw new Exception($errinfo[2], $errinfo[0]);
            }
            ++$idx;
        }
        $result = $stmt->execute();
        if (!$result) {
            $errinfo = $stmt->errorInfo();
            throw new Exception($errinfo[2], $errinfo[0]);
        }
        return $result;
    }

    public function execute($sql, $params = array()) {
        $stmt = $this->_db->prepare($sql);
        return $this->_execStatement($stmt, $params);
    }

これでPHPの値の型に合わせてPDOStatement::bindValue()に適切なフラグをセットするようになったので、BOOLEANのFALSE値も問題なく動作するようになりました。

PDO対応にしてよかったこと

速くなった
  • PDO は Extension ベースのライブラリですから、ピュア PHP で書かれた Creole より数割〜数倍スピードが速くなりました。
BOOLEAN (TRUE / FALSE) と NULL 値がきちんと取れるようになった
  • PHP の pgsql モジュールは、BOOLEAN 値を取得するとあろうことか 't' / 'f' という文字列で返ってきます。PDO_pgsql ではきちんと型情報を見て PHP の bool 値に変換されて返ります。
  • 同様に pgsql モジュールでは NULL 値と空文字列の判別ができませんでしたが、PDO_pgsql ではきちんと NULL は NULL として返ります。

おまけ

MySQL では BOOLEAN は TINYINT で実装されているので、そもそもこの問題は発生しないのでした、ちゃんちゃん。

次(もう今日だ)はid:heavenshellさんです。どうもお待たせしましたw