ホーム > CakePHP > CakePHP 1.3で大量のクエリを投げるときの注意点

CakePHP 1.3で大量のクエリを投げるときの注意点

cakephp_query_shell.png

今日は簡単な記事です。
自分がハマったので、他の人がハマらないように情報共有を。

CakePHPネタで、特にシェルを使っている人向けの記事です。
興味がない方はスルーしてください。

CakePHPで大量のクエリを投げる

CakePHPで、データベースに対して大量のクエリを投げたことがありますか?
お知らせメールでは、シェルもCakePHPで書いています。
そのため、一度の実行で大量のクエリが発行されることがあります。

クエリといっても、CakePHPのラッパーのModel::find()Model::save()のことです。
これをレコード分繰り返すことがあります。
この場合、一回の実行あたりのクエリ発行数は数千から数万に。

このような処理を書いていると、どうもおかしな現象に出くわします。

Warning: SQL Error: 1054: Unknown column 'ComicAuthorMergeQueue.base_id' in 'where clause' in /usr/lib/cakephp1.3/cake/libs/model/datasources/dbo_source.php on line 673
Query: SELECT `SearchIndex`.`id`, `SearchIndex`.`search_index` FROM `search_indices` AS `SearchIndex`   WHERE `ComicAuthorMergeQueue`.`base_id` = 6899

ん…こんなクエリ発行した覚えはないんだけど?

初めはモデル周りの参照の関係が原因だと考えました。
しかし、実はもっと根が深い問題で、CakePHPのキャッシュが絡んでいました。

CakePHPがWHEREをキャッシュしている

CakePHPはデフォルトでWHERE文をキャッシュしています
例として、Model::find()を取り上げます。

$params = array(
	'conditions' => array('id = ' => 1000));
$this->User->find('all', $params);
SELECT * FROM `users` AS `User` WHERE `User`.`id` = 1000;

この”‘conditions’ => array(‘id = ‘ => 1000)“と”WHERE `User`.`id` = 1000“の対応関係をキャッシュするのです。
キャッシュする場所はインスタンスのプロパティ。つまりメモリキャッシュです。
そのため、二度目に同じ条件のModel::find()を呼んだ場合、SQLは動的に生成されません。
メモリキャッシュからWHERE文を取り出し、そのまま使用します。

/**
 * Returns a quoted name of $data for use in an SQL statement.
 * Strips fields out of SQL functions before quoting.
 *
 * @param string $data
 * @return string SQL field
 * @access public
 */
  function name($data) {
    if (is_object($data) && isset($data->type)) {
      return $data->value;
    }
    if ($data === '*') {
      return '*';
    }
    if (is_array($data)) {
      foreach ($data as $i => $dataItem) {
        $data[$i] = $this->name($dataItem);
      }
      return $data;
    }
    $cacheKey = crc32($this->startQuote.$data.$this->endQuote);
    if ($return = $this->cacheMethod(__FUNCTION__, $cacheKey)) {
      return $return;
    }
/**
 * Cache a value into the methodCaches.  Will respect the value of DboSource::$cacheMethods.
 * Will retrieve a value from the cache if $value is null.
 *
 * If caching is disabled and a write is attempted, the $value will be returned.
 * A read will either return the value or null.
 *
 * @param string $method Name of the method being cached.
 * @param string $key The keyname for the cache operation.
 * @param mixed $value The value to cache into memory.
 * @return mixed Either null on failure, or the value if its set.
 */
  function cacheMethod($method, $key, $value = null) {
    if ($this->cacheMethods === false) {
      return $value;
    }
    if ($value === null) {
      return (isset($this->methodCache[$method][$key])) ? $this->methodCache[$method][$key] : null;
    }
    return $this->methodCache[$method][$key] = $value;
  }

ちょっと省略していますが、このコードがそのキャッシュに当たる部分です。
詳しく調べたい方はdbo_source.phpのDboSource::cacheMethod()を追ってください。

ここで見て分かる通り、crc32()を使ってハッシュを管理しています
実はこのcrc32()を使ったハッシュは数万件のデータで衝突が発生するようです。

衝突が起こると、まったく無関係のWHERE文がModel::find()やModel::save()に対して呼ばれます
その結果が冒頭のエラーです。

キャッシュをオフにする

この現象を突き止めるまでは色々と調べて回りました。
この情報に辿りつくまでに半日ぐらい掛かりました。

CakePHPのチケットです。
このチケットには、こうあります。

I think documentation is a good approach. Fully unique hashes can be slower than crc32 which is why it was chosen. Disabling the cache when its not helpful and providing good documentation on how to do that is probably more pertinent.

#870 Issues with DboSource::$methodCache – CakePHP – cakephpより

これに従って修正されたAPIドキュメントがこれです。

Caches result from query parsing operations. Cached results for both DboSource::name() and DboSource::conditions() will be stored here. Method caching uses `crc32()` which is fast but can collisions more easily than other hashing algorithms. If you have problems with collisions, set DboSource::$cacheMethods to false.

Commit e023350af57e07ac3351ab86c07361697fc1b369 to cakephp’s cakephp – GitHubより

なるほど。
DboSourceクラスcacheMethodsプロパティをfalseに設定すれば良いみたいです。
そうすれば、このキャッシュはオフになると。

余談ですが、CakePHP側ではこの部分の衝突を問題にしていないようですね。
パフォーマンスの問題と、使用方法を天秤にかけたのでしょう。
CakePHPはWebフレームワークで、シェルの重たい処理なんて想定してないでしょうから。

シェルでこのキャッシュ機能をオフにするために、AppShellクラスを作ります。

<?php
App::import('Shell', 'Shell');

class AppShell extends Shell {
  var $cacheMethods = false;

  function initialize() {
    $db =& ConnectionManager::getDataSource('default');
    $db->cacheMethods = $this->cacheMethods;
    parent::initialize();
  }
}

あとは、これを各シェルのクラスが継承すればOKです。

<?php
App::import('Shell', 'AppShell');

class EggShell extends AppShell {
}

キャッシュをオンにしたいシェルがある場合は、こう書きます。

<?php
App::import('Shell', 'AppShell');

class EggShell extends AppShell {
  var $cacheMethods = true;
}

これでOKです。
ここではシェルクラスにAppShellを継承させる形で解決しましたが、これはCakePHP 1.3.x限定です。
CakePHP 1.2.xではシェルクラスに自分で作ったクラスを継承させることができません(厳密に言えばできるのですが、色々と動かなくなるのでそれの対処が面倒くさい)。

ちなみに、この内容はPHP MatsuriのJIREI Nightで話した内容です。

JIREI NIGHT お知らせメール

もし、同じ現象でハマった場合は試してみてください。

こちらもあわせてどうぞ

ちょっと一言

最近ちょっとお疲れです。
大した仕事してないんですけどね。
マイペースでやっていきましょう。でないとバテちゃいますよ(私みたいに)。

  • http://topsy.com/1-byte.jp/2010/10/29/too_many_queries_on_cakephp/?utm_source=pingback&utm_campaign=L2 Tweets that mention CakePHP 1.3で大量のクエリを投げるときの注意点 — Topsy.com

    [...] This post was mentioned on Twitter by hampom, 城介(じょうのすけ) and CakePHP Retweet Ja, tfmagician(FUJIWARA). tfmagician(FUJIWARA) said: ブログ更新しました!: CakePHP 1.3で大量のクエリを投げるときの注意点 http://1 [...]

  • http://blog.orgal.jp/wp/?p=160 ORGAL BLOG » Blog Archive » CakePHP (1.3) 大量のクエリを投げるときの注意点

    [...] http://1-byte.jp/2010/10/29/too_many_queries_on_cakephp/ 大量にクエリを発行する際、キャッシュが短い周期で被っちゃうみたいです。 「数万件のデータで衝突が発生する」とのことで、大量クエリが [...]

  • uchida

    原因不明のバグで苦労しました
    本当に助かりました!

blog comments powered by Disqus

ホーム > CakePHP > CakePHP 1.3で大量のクエリを投げるときの注意点

スポンサードリンク
書いている人
つぶやき
RSS 気になるニュース
過去の記事

ページの上部に戻る