Ethna の AppManager の getObjectPropList() で取得するカラムが異なるのに検索条件が同じだとキャッシュが返ってくるのを治す

どうもどうもこんにちは。今日は Ethna ネタですよ。

僕はどちらかというと AppObject を返してくれる getObjectList() 派なのですが、パフォーマンスが気になるときは AppObject を生成せず、カラムを制限して取得できる getObjectPropList() を使った方が良いケースもあったりします。その getObjectPropList() のキャッシュ周りでつまづいたのでそのメモでも。

サンプルとして Manager 側に id だけ取得する getIdList() と name だけ取得する getNameList() を実装してみます。

<?php
class Appid_HogeManager
{
    public function getIdList()
    {
        return $this->getObjectPropList('Hoge', array('id'), null, null, 0, 10);
    }
    public function getNameList()
    {
        return $this->getObjectPropList('Hoge', array('name'), null, null, 0, 10);
    }
}

データベースにはあらかじめこんな感じのデータを入れておきます。

mysql> select * from hoge;
+----+----------------------------------------------+
| id | name                                         |
+----+----------------------------------------------+
|  1 | Akufen - My Way                              |
|  2 | Todd Edwards - Full On                       |
|  3 | Emerson, Lake & Palmer - Brain Salad Surgery |
+----+----------------------------------------------+
3 rows in set (0.01 sec)

Action から Manager のコードを呼び出します。しかしこのコードは期待どおりの動作をしません。

<?php
    ....
    function perform()
    {
        $manager = $this->backend->getManager('Hoge');
        $hoge_id_list = $manager->getIdList();
        $hoge_name_list = $manager->getNameList();

        $this->backend->log(LOG_DEBUG, var_export($hoge_id_list, true));
        $this->backend->log(LOG_DEBUG, var_export($hoge_name_list, true));

        ....
    }

アプリケーションのログを見るとクエリがいちどきりしか実行されていなくて二つのメソッドの戻り値が同じであることが分かります。

2010/04/07 17:44:48 Appid(DEBUG): SELECT COUNT(DISTINCT `hoge`.`id`) AS `id_count` FROM `hoge`
2010/04/07 17:44:48 Appid(DEBUG): SELECT `hoge`.`id` FROM `hoge`   LIMIT 10 OFFSET 0
2010/04/07 17:44:48 Appid(DEBUG): hoge_id_list = array (
  0 => '3',
  1 =>
  array (
    0 =>
    array (
      'id' => '1',
    ),
    1 =>
    array (
      'id' => '2',
    ),
    2 =>
    array (
      'id' => '3',
    ),
  ),
)
2010/04/07 17:44:48 Appid(DEBUG): hoge_name_list = array (
  0 => '3',
  1 =>
  array (
    0 =>
    array (
      'id' => '1',
    ),
    1 =>
    array (
      'id' => '2',
    ),
    2 =>
    array (
      'id' => '3',
    ),
  ),
)

Ethna_AppManager の実装を見てみます。

    function getObjectPropList($class, $keys = null, $filter = null,
                               $order = null, $offset = null, $count = null)
    {
        global $_ETHNA_APP_MANAGER_OPL_CACHE;

        $prop_list = array();
        $class_name = sprintf("%s_%s", $this->backend->getAppId(), $class);

        // キャッシュチェック
        $cache_class = strtolower($class_name);
        if (is_array($_ETHNA_APP_MANAGER_OPL_CACHE) == false
            || array_key_exists($cache_class, $_ETHNA_APP_MANAGER_OPL_CACHE) == false) {
            $_ETHNA_APP_MANAGER_OPL_CACHE[$cache_class] = array();
        }
        $cache_key = serialize(array($filter, $order, $offset, $count));
        if (array_key_exists($cache_key, $_ETHNA_APP_MANAGER_OPL_CACHE[$cache_class])) {
            // skip
        } else {
            // キャッシュ更新
            $tmp =& new $class_name($this->backend);
            $_ETHNA_APP_MANAGER_OPL_CACHE[$cache_class][$cache_key]
                = $tmp->searchProp($keys, $filter, $order, $offset, $count);
        }

        return $_ETHNA_APP_MANAGER_OPL_CACHE[$cache_class][$cache_key];
    }

キャッシュを返す条件が $filter, $order, $offset, $count の指定が同様な場合のようです。異なる $keys が指定された場合はキャッシュを使わないように getObjectPropList() メソッドをオーバーライドして挙動を変更してしまいます。オーバーライドしたら controller への require と Appid_HogeManager の extends 先の変更を忘れずに!

<?php
class Appid_AppManager extends Ethna_AppManager
{
    function getObjectPropList($class, $keys = null, $filter = null,
                               $order = null, $offset = null, $count = null)
    {
        global $_ETHNA_APP_MANAGER_OPL_CACHE;

        $prop_list = array();
        $class_name = sprintf("%s_%s", $this->backend->getAppId(), $class);

        // キャッシュチェック
        $cache_class = strtolower($class_name);
        if (is_array($_ETHNA_APP_MANAGER_OPL_CACHE) == false
            || array_key_exists($cache_class, $_ETHNA_APP_MANAGER_OPL_CACHE) == false) {
            $_ETHNA_APP_MANAGER_OPL_CACHE[$cache_class] = array();
        }
        $cache_key = serialize(array($keys, $filter, $order, $offset, $count));
        if (array_key_exists($cache_key, $_ETHNA_APP_MANAGER_OPL_CACHE[$cache_class])) {
            // skip
        } else {
            // キャッシュ更新
            $tmp =& new $class_name($this->backend);
            $_ETHNA_APP_MANAGER_OPL_CACHE[$cache_class][$cache_key]
                = $tmp->searchProp($keys, $filter, $order, $offset, $count);
        }

        return $_ETHNA_APP_MANAGER_OPL_CACHE[$cache_class][$cache_key];
    }
}

治ったよー。

2010/04/07 17:49:10 Appid(DEBUG): SELECT COUNT(DISTINCT `hoge`.`id`) AS `id_count` FROM `hoge`
2010/04/07 17:49:10 Appid(DEBUG): SELECT `hoge`.`id` FROM `hoge`   LIMIT 10 OFFSET 0
2010/04/07 17:49:10 Appid(DEBUG): SELECT COUNT(DISTINCT `hoge`.`id`) AS `id_count` FROM `hoge`
2010/04/07 17:49:10 Appid(DEBUG): SELECT `hoge`.`name` FROM `hoge`   LIMIT 10 OFFSET 0
2010/04/07 17:49:10 Appid(DEBUG): hoge_id_list = array (
  0 => '3',
  1 =>
  array (
    0 =>
    array (
      'id' => '1',
    ),
    1 =>
    array (
      'id' => '2',
    ),
    2 =>
    array (
      'id' => '3',
    ),
  ),
)
2010/04/07 17:49:10 Appid(DEBUG): hoge_name_list = array (
  0 => '3',
  1 =>
  array (
    0 =>
    array (
      'name' => 'Akufen - My Way',
    ),
    1 =>
    array (
      'name' => 'Todd Edwards - Full On',
    ),
    2 =>
    array (
      'name' => 'Emerson, Lake & Palmer - Brain Salad Surgery',
    ),
  ),
)

そもそも同様の条件のクエリを複数回発生するコードを書く事自体が問題な気もしますが、複数のメソッドにまたがって getObjectPropList() を使っていると結構ハマりそうな仕様かなと思いました。まる。

preload preload preload