Apelog

RailsやCakePHPなどのフレームワークではお馴染みのfindBy系メソッドをZendFrameworkで実装したいということで。

Zend_Db_Tableクラスを継承したMyZend_Db_Tableクラス内のマジックメソッドで実装し、ModelはMyZend_Db_Tableを継承する。

<?php
class MyZend_Db_Table extends Zend_Db_Table {
    /**
     * 論理削除カラム名
     * @var string
     */
    private $_deleteCol = 'delete_time';

    /**
     * findAllBy[ColName]($value [, $flag])でデータアクセス
     *
     * $flag = true で論理削除された行は除外して取得
     *
     * @param string $method
     * @param array  $args
     * @return Zend_Db_Table_Row_Abstract
     */
    public function __call($method, $args) {
        if (preg_match('/^findAllBy(\w+)?$/', $method, $matches)) {
            $field = strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $matches[1]));

            $where = array();
            $where["`$field` = ?"] = $args[0];

            if (isset($args[1]) && $args[1] === true
                && in_array($this->_deleteCol, $this->_cols)) {
                $where[] = "`{$this->_deleteCol}` IS NULL";
            }
            return $this->fetchAll($where);
        }
        throw new Zend_Db_Table_Exception("Call undefine method $method()");
    }
}

これで$model->findAllByName(’Design’);のようにアクセスすれば`name` = ‘Design’という条件でのデータアクセスが可能になる。

データは論理削除する事が多いのでfindAllByName(’Design’, true)の形の呼び出しで
`name` = ‘Design’ AND `delete_time` IS NULL

有効な行のみ返すようにしている。

まぁそんな感じで移植実装がおこなわれていくわけです。

MySQLではインデックスを使用しても30%以上のレコードにアクセスしなければならない時インデックスは使用されない。が、LIMIT句があればインデックスを使用する。

MySQL では利用可能な場合でもインデックスが使用されない場合があることに注意してください。この一例として、インデックスの使用によって、MySQL がテーブルの 30% を超えるレコードにアクセスする必要が生じる場合が挙げられます(この場合は、必要なシークが大幅に減少するため、テーブルスキャンのほうが高速になる可能性が高くなります)。 ただしこのクエリに、レコードの一部のみを取り出す LIMITが使用されている場合、結果で返される少数のレコードを迅速に検索できるため、MySQL はインデックスを使用します。

6.4.5. MySQLにおけるインデックスの使用

例えば’2007-01-01 < register_date < 2007-12-31′な数百万件データがありregister_dateにインデックスを張っているものとして、以下のSQLの条件の日付を進めていくと急にインデックスが効かなくなる。

SELECT * FROM `foo` WHERE `register_date` < '2007-01-01'

知っていればどうという事はないものの何も考えず実装してしまうとデータ量が増えると予想外の時間が掛ってしまう時があるかもしれない。データ量を増やしたテストとexplainは重要。

データ量が増えるなら差分のみを対象に出来る設計にした方が良いと思う。

Smarty互換でSmartyより軽量なTemplateLiteを使う
に書いた後適度に使っていたところ、Smartyと決定的な差異を発見。

TemplateLiteはオブジェクトにアクセスできない。はじめからオブジェクトをアサインしないような作りにしておけば問題無いものの、既にSmartyでオブジェクトをアサインして使っている場合、テンプレートエンジンをそのまま差し替えるわけにはいかないので注意。

例えば、Zend_Validateのエラー結果を纏めて以下のようなクラスに渡したオブジェクトをアサインしているとする。

<?php
/**
 * @package Sample
 */
class Sample_Validate_Result {
    /**
     * @var array
     */
    private $_errors;

    /**
     * @param array $errors
     */
    public function __construct(array $errors) {
        $this->_errors = $errors;
    }

    /**
     * エラーの有無を返す
     *
     * @param string $name
     * @return mixed
     */
    public function __get($name) {
        if (array_search($name, $this->_errors) !== false) {
            return true;
        } elseif (!array_key_exists($name, $this->_errors)) {
            return false;
        } else {
            return new Sample_Validate_Result($this->_errors[$name]);
        }
    }
}

その状態でテンプレートからアサインしたオブジェクトのメンバ変数にアクセスしようとTemplateLiteの場合、evalに失敗してエラー。

<?php
// Validatorから帰ってくるエラー例
$errorArray = array(
    'nickname'    => array('isEmpty'),
    'mailaddress' => array('tooLong', 'invalid')
);

// コントローラー内でエラーオブジェクトをアサイン
$this->view->errors = new Sample_Validate_Result($errorsArray);

// Smartyならこれができる
{ if $errors->nickname->isEmpty }

完全互換だと思っていて差し替えが後になってしまうと、影響範囲も広がってしまうので記憶の片隅にでも。