リレーションとキャッシュ

Laravel4のActive Record (Eloquent)では、リレーションを貼ると、初回参照時にデータベースを読みに行き(Lazy Loading)、以後は読みだした値をキャッシュして再利用するようになる。
そして、hasOneやhasManyなどでは、保存/更新を行っても、キャッシュはクリアされない。

このため、hasOneやhasManyで
リレーションの読出し => リレーションの追加(変更) => リレーションの読出し
の一連の動作を行うと、2回目の読出しは、リレーションの追加(変更)が反映されていないものになってしまう。


特に、もともと初期にリレーションがなかった場合(null)の場合は、2回目でもnullのままなので、
呼び出そうとして
Trying to get property of non-object
が出てしまうので注意が必要。


例として、EntryクラスとPageViewクラスがあり、それぞれ

<?php
class Entry extends Eloquent
{
	public function pageView() 
	{
		return $this->hasOne('PageView','entry_id');
	}
 ...
}

class PageView extends Eloquent {
	protected $table = 'page_views';	
	// page_viewsテーブルに、ctというカラムがある

	public function entry() {
		return $this->belongsTo('Entry');
	}
	
 ...
}
?>

のように定義されているケースを考える。

このとき、「もしentryにpageViewがなければpageView新規作成する」コードを書こうとして次のように記述すると、

<?php
$entry = Entry::find($id);
$pageView = $entry->pageView; // この読出しでキャッシュされる
if (!isset($pageView)) {
  $pageView = new PageView();
  $pageView->ct = 1;
  $entry->pageView()->save($pageView);  // pageViewのセットしても、キャッシュは更新されない
}

echo $entry->pageView->ct; // キャッシュされたものを呼び出す。nullの場合、エラーになる。
?>

最終行で
Trying to get property of non-object
エラーになってしまう。


これは、2行目で一度、$entry->pageViewでpageViewを参照しているため、それがnullであった場合でも
nullであることがキャッシュされてしまい、その後pageViewのセットをしても、キャッシュは更新されないためである。


これを防ぐためには、キャッシュをクリアすればよいので、

<?php
$entry = Entry::find($id);
$pageView = $entry->pageView;
if (!isset($pageView)) {
  $pageView = new PageView();
  $pageView->ct = 1;
  $entry->pageView()->save($pageView); 
  $entry->setRelations( array() );  // キャッシュのクリア
}

echo $entry->pageView->ct;
?>

のようにすれば、最終行ではpageViewをデータベースから読み直すので、問題なく動作する。


とはいえ、この仕様は正直ちょっと不便を感じます。
オプションでも良いので、リレーションで子を追加/変更したときは、親にも反映して欲しいです。