PHP
PostgreSQL
PDO
cakephp2

cakephp1.2からcakephp2.10に上げた時のモデルでの違いでエラー1

cakephp1.2からcakephp2系にあげて、エラーになったことと対処した内容です。

postgres関数からPDOに変わっていたこともあり、実装がかなり変わっています。

プリペアドステートメントに変わった影響

cakephp1.2は、変数を自前で変換してエスケープしてSQLを実行している実装になっていた。

cakephp2.10ではPDOのプリペアドステートメントを利用しているので、実装はDBのエンジンごとにまとまった実装になっていると思います。

いまのところエラーになった部分をいくつか対処した内容です。

queryの変数のパラメータで余計な内容はNG

このプログラムが1.2ではOKで、2系ではエラーになります。

test.php
$sql = "SELECT * FROM users WHERE id = :id ";

$params = [
    'id' => 1
    , 'company_id' => 2
];

$this->query($sql, $params);

理由としてはPDOの利用に代わり、PDO自体が余計なパラメータを許容していないからです。

むしろ便利にしていたのは1.2では自前でパラメータの部分を作成していたため、動作する仕様だったみたいです。

cake/libs/model/datasources/dbo_source.php
return $this->fetchAll(String::insert($args[0], $args[1]), $cache);
cake/libs/string.php
function insert($str, $data, $options = array()) {
        $defaults = array(
            'before' => ':', 'after' => null, 'escape' => '\\', 'format' => null, 'clean' => false
        );
        $options += $defaults;
        $format = $options['format'];

        if (!isset($format)) {
            $format = sprintf(
                '/(?<!%s)%s%%s%s/',
                preg_quote($options['escape'], '/'),
                str_replace('%', '%%', preg_quote($options['before'], '/')),
                str_replace('%', '%%', preg_quote($options['after'], '/'))
            );
        }
        if (!is_array($data)) {
            $data = array($data);
        }

        if (array_keys($data) === array_keys(array_values($data))) {
            $offset = 0;
            while (($pos = strpos($str, '?', $offset)) !== false) {
                $val = array_shift($data);
                $offset = $pos + strlen($val);
                $str = substr_replace($str, $val, $pos, 1);
            }
        } else {
            asort($data);

            $hashKeys = array_map('md5', array_keys($data));
            $tempData = array_combine(array_keys($data), array_values($hashKeys));
            foreach ($tempData as $key => $hashVal) {
                $key = sprintf($format, preg_quote($key, '/'));
                $str = preg_replace($key, $hashVal, $str);
            }
            $dataReplacements = array_combine($hashKeys, array_values($data));
            foreach ($dataReplacements as $tmpHash => $data) {
                $str = str_replace($tmpHash, $data, $str);
            }
        }

        if (!isset($options['format']) && isset($options['before'])) {
            $str = str_replace($options['escape'].$options['before'], $options['before'], $str);
        }
        if (!$options['clean']) {
            return $str;
        }
        return String::cleanInsert($str, $options);
    }

対処

パラメータを条件関係なくいれれたから楽だったんですが、変わったのは仕方がないです。

対処としては、呼び出しているところでパラメータを必要な時だけパラメータにする。

ただ呼び出し箇所が多かったので、ソースで対処してみました。

lib/Cake/Model/DataSource/DboSource.php
protected function _execute($sql, $params = array(), $prepareOptions = array()) {
    $sql = trim($sql);
    if (preg_match('/^(?:CREATE|ALTER|DROP)\s+(?:TABLE|INDEX)/i', $sql)) {
        $statements = array_filter(explode(';', $sql));
        if (count($statements) > 1) {
            $result = array_map(array($this, '_execute'), $statements);
            return array_search(false, $result) === false;
        }
    }

    try {
        $query = $this->_connection->prepare($sql, $prepareOptions);
        $query->setFetchMode(PDO::FETCH_LAZY);
        if (!$query->execute($this->_executeParam($sql, $params))) {
            $this->_result = $query;
            $query->closeCursor();
            return false;
        }
        if (!$query->columnCount()) {
            $query->closeCursor();
            if (!$query->rowCount()) {
                return true;
            }
        }
        return $query;
    } catch (PDOException $e) {
        if (isset($query->queryString)) {
            $e->queryString = $query->queryString;
        } else {
            $e->queryString = $sql;
        }
        throw $e;
    }
}


protected function _executeParam( $sql, $params ){
    if(empty($params)) return $params;
    if(!is_array($params)) return $params;

    // ソート
    uksort($params, function($a, $b){
        $al = mb_strlen($a);
        $bl = mb_strlen($b);
        return $al >= $bl ? ($al == $bl ? 0 : -1 ) : 1;
    });

    $ret = [];
    foreach($params as $key => $param){
        if( is_numeric($key) ) {
            $ret[$key] = $param;
            continue;
        }

        if( strpos ($sql, ":".$key ) !== false) {
            $ret[$key] = $param;
        }
    }
    if(empty($ret)) return[];

    return $ret;
}

単純に文字列からパラメータがある場合にのみ、パラメータとして渡すように変更してみました。

一応動作しています。