FuelPHP

FuelPHPのOrmはキャッシュが有効なせいで思わぬ不具合を引き起こす場合があったりする

■キャッシュとは
何らかのデータにアクセスした際、2度目以降の参照でより高速にアクセスできるような方法でそのデータを複製しておく仕組みを、キャッシュと呼びます。
先に前提知識の確認のため、PHPでの簡単なキャッシュの実装を示します。
例えばDBへの問い合わせのような重い処理で手に入れたデータを複数回利用する場合、一度取得した結果は配列に格納することで、2度目以降は実際のDBへのアクセスを行わず、高速にそのデータを参照することができる。

(参考:http://tech.respect-pal.jp/fuelphp-orm-cache/)

 

■参考コード

class Model_Test extends \Orm\Model
{
    private static $cache = [];

public function get_by_id($id)
{
if (!isset(static::$cache[$id]))
{
static::$cache[$id] = static::query()
->where('id', $id)
->get_one();
}
return static::$cache[$id];
}
}

 

rmのオブジェクトキャッシュはModelに設定されたプライマリキーの値*1をキー、クエリに伴って構築したOrmオブジェクトを値とする連想配列で保持されています。

Ormオブジェクトは

  • find()やquery()で最初にレコードを取り出した際
  • save()でDBへのINSERTが行われる際

にキャッシュへ登録され、2度目以降に同じレコードのオブジェクトをプライマリキーのみでfind()して取得する際、すなわちfind($pk)はほぼ配列からデータを取り出すだけの処理となり、非常に高速に動作します。しかし、純粋に速度的な面でキャッシュが有効に使われ、まともな効果が出るのはこのケースだけです。

$test = Model_Test::find(1); // 1回目はDBにクエリを飛ばす
$test2 = Model_Test::find(1); // 2回目以降はキャッシュから取り出すだけで超速い

 

しかし基本的に、この際のキャッシュ参照は速度的な面において、プライマリキーでのfind()のような劇的な効果をもたらすものではありません。ここでのキャッシュの参照は実際にDBへのクエリを飛ばしデータを取得した後、メモリのどこへ取得したデータを放り込むかを決めるものであって、DBアクセスはバイパスされず、もっぱらオブジェクトの使い回しによるメモリ使用量削減とオブジェクト生成処理省略の効果しか出ないということです

 

プライマリキー以外を条件にデータを取得する際はもちろん、たとえ結果的にプライマリーキーのみを条件にデータを取得する場合であっても、find($pk)の形でただ1つの引数のみを与える使い方以外では、高速なキャッシュの参照は行われません。

$test = Model_Test::find(1); // クエリが飛ぶ
$test = Model_Test::find('first', [ // クエリが飛ぶ
 'where' => [
 ['id' => 1]
 ]
]);
$test = Model_Test::query() // 何かしらのユニークキーで取得
 ->where('unique_key', 1)
 ->get_one();
$test = Model_Test::query()
 ->where('unique_key', 1) // 同じユニークキーで取得しても当然クエリが飛ぶ
 ->get_one();

 

プライマリキーだけではなく他のユニークキーを使ってキャッシュの恩恵を得たい場合や、ある程度複雑なクエリを飛ばす回数をキャッシュで減らしたい場合は、自前で取得結果のキャッシュを行う必要がある。

 

■Ormのキャッシュをクリアする方法
FuelPHPのOrmでは、デフォルトの機能としてはキャッシュをクリアする方法が提供されていません。

このため、たとえばバッチ処理にOrmが使用され、数十万ものOrmオブジェクトを生成するようなことがあった場合、少し悲しいことが起きるかもしれません。Ormの利用コード側が各オブジェクトの参照を手放した後も、キャッシュが参照を握り続けることによって、GCによるメモリ領域の回収が行われず、PHPの実行環境で許容されるメモリ容量を食い潰してしまう可能性があります。

foreach ($超たくさんのid as $id)
{
 // キャッシュにOrmオブジェクトが登録される
 $test = Model_Test::find($id);
 
 // キャッシュが参照を握ってるので、unsetや次のループで$testを上書きしてもGCされない
 unset($test);
} // 実行しおわる前にPHPがメモリ容量不足で死ぬ

 

そもそも、バッチ処理にOrmのような重い方法を使おうとするのは賢い選択とは言えません。が、たとえ性能が犠牲になったとしても、なるべくクエリビルダや生SQLといった他の手段の利用を避けたいケースはあり得ます。たとえば、システムの他の部分での利便性のためにOrmを採用していて、Model側の処理の多くがOrm特有の機能(Observerやリレーション定義)に依存している場合など

現状の内部実装に依存した方法であってもよいので、とにかくキャッシュをクリアしながらOrmを利用したいという場合、オブジェクトキャッシュはOrm\Modelのprotectedメンバ$_cached_objectsに連想配列として保持されているため、継承でこれを空にするメソッドを加えることで、キャッシュをクリアすることができます

class Model_Test extends \Orm\Model
{
 public static clear_cache()
 {
 static::$_cached_objects[get_called_class()] = [];
 }
}

 

データの取得時にキャッシュを無視する(無効化する)方法
データの取得時に、一旦オブジェクトキャッシュを無視したいという場合があります。

例えばFuelPHPのOrmはfind()やquery()の際にselectを使うことができ、一部のカラムについてのみデータを取得することで、DBからフェッチするデータの量を減らして最適化することができます。

$test = Model_Test::query()
 ->select('id') // 「id」カラムのみフェッチ、他のプロパティはnullになる
 ->where('id', 1)
 ->get_one();

 

問題は、この一部のカラムについてのみデータを取得し構築したOrmオブジェクトが、やはりキャッシュに登録されてしまうということです。

処理のはじめの方でselectによってカラムを絞り、半端に構築したOrmオブジェクトが、後でカラムを絞らずにプライマリキーで()find()しようとした際にもキャッシュから取得されるため、本来有効な値の入っているべきプロパティからnullが取得されるというような、予想外のバグを引き起こす可能性があります

他にもアプリケーション側の何らかの都合によって、Ormを通したデータ取得とOrmを通さないデータ更新が交互に行われるようなことがあった場合、実際のDBデータとキャッシュで内容にズレが生じてしまうことになるため、この場合もキャッシュから取得されるデータを使うのはバグの元になります。

また当然、Ormによるデータ更新をDB::rollback_transaction()で巻き戻した場合も、オブジェクトキャッシュの内容は巻き戻らず、DBデータとのズレが発生します。

最初にselectでカラムを絞って取得した後、次にfind($pk)の形で別のselect条件で同じレコードのOrmオブジェクトを取得する際は、先に述べた方法でキャッシュをまるごと破棄してしまうのも1つの対処法です。ただそこまでしなくとも、find()やquery()に与える設定でfrom_cache(false)を使うことで、データ取得の際にクエリ単位でキャッシュを無視させることもできます

$partial = Model_Test::query() // 「id」カラムのみフェッチ
 ->select('id')
 ->where('id', 1)
 ->get_one();
 
$complete = Model_Test::query() // キャッシュを無視して取得
 ->where('id', 1)
 ->from_cache(false)
 ->get_one();

 

1・プライマリキーのみでのfind()以外の場合については特別に対策されている筈なのですが(*)、実際にはやはりキャッシュされた内容だけが取得されます。なんとなくバグな気がします。
2・from_cache()は、こういったキャッシュ問題へのworkaroundとしてOrmへ後付けされたものです

ORMは便利なツールである一方、使ってよいケースとダメなケースとがあり、ダメなケースの多くは性能的な問題の解決が困難となるケースです。

 

 


Warning: Undefined variable $postID in /home/foodheart/flashbuilder-job.com/public_html/wp-content/themes/stingerplus/single.php on line 87

-FuelPHP