03.042013

Erweiterte Cachegroups mit CakePHP

Ich habe in den letzten Wochen mich ein wenig in meiner Freizeit mit CakePHP beschäftigt und ebenso mit dem Thema Caching und Cachegroups. Hier bin ich auf ein paar Probleme bzw. Grenzen gestoßen, die mir aufgefallen sind und will euch dabei teilhaben lassen, wie ich diese gelöst habe.

Zum einen cached CakePHP ja selbst schon einiges, wenn ich das richtig verstanden habe, aber man kann dies noch explizit steuern, indem man SQL Abfragen selbst cached, um die Datenbanklast so minimal wie möglich zu lassen. Wie macht man dies nun aber am sinnvollsten? Schnell war die Antwort darauf gefunden, Cachegroups. Nur wie funktionieren sie richtig und wie kann man diese vernünftig benutzen?

Nehmen wir das Beispiel von CakePHP aus dem Anfangstutorial mit dem Blog. Diesen Blog wollen wir gecached haben, das geht ganz einfach, indem wir die find-Methode in AppModel überschreiben. Hier kommt die Cachinglogik hinein.

Zuerst benötigen wir ein paar global benutzbare Variabeln:

protected $cache = false;
protected $cachegroup = null;
protected $cachename = '_findcache_';

Außerdem benötigen wir 2 bzw. 3 zusätzliche Funktionen. Zum einen die Funktion setCache, die wie folgt aussieht:

public function setCache($value = false, $group = false) {
  $config = array(
    'engine' => 'File',
    'prefix' => $this->cachename,
    'path' => CACHE . $this->cachename . DS,
    'serialize' => true,
    'duration' => '+5 days',
  );
  if ($group) {
    $this->cachegroup = $group;
    $config['groups'] = array($group);
  }
  Cache::config($this->cachename, $config);
  $this->cache = $value;
}

Hier drin bestimmen wir, welche Config geladen werden soll. Man kann hier auch noch weiter experimentieren mit mehreren Cachenamen etc. aber für dieses Beispiel, was nur zum Verdeutlichen der CakePHP Cachegroups dient, reicht dies aus. Es wird also in die Config bei Aufruf der Funktion erstellt und geschrieben. Hier kann man außerdem den Engine selbst auswählen, den man haben möchte und eine max-duration. Man kann hier natürlich auch noch zusätzliche Übergabeparameter einbauen, mit denen man den Timeout zusätzlich bestimmt. Wenn der Übergabeparameter $group mit übergeben wurde, wird dieser auch noch gesetzt. Wenn man dies nicht setzt, wird allgemeines caching betrieben, was automatisch nach dem timeout erlischt.

Beim Thema Caching muss man sich natürlich auch gedanken machen, dass man immer aktuelle Daten hat, also benötigen wir noch die Hilfsfunktion invalidateCacheGroup:

function invalidateCacheGroup($groupname = null) {
  if ($groupname) {
    Cache::clearGroup($groupname);
  }
}

Dann wären wir hierbei auch schon fertig und müssten nur noch die Find Methode überschreiben. Grundsätzlich sollte auch das Caching, wenn man dies möchte, nur im Debugmodus 0 benutzt werden. Wenn dies nicht erwünsch ist, muss man das folgende einfach rausnehmen, dann wird immer gecached.

function find($type, $params = null) {
  if ($this->cache AND Configure::read('debug') == 2) {
    //Hier kommt die Cachemagic rein
  } else {
    return parent::find($type, $params);
  }
}

Das grundsätzliche caching wird folgendermaßen gemacht. Es wird zuerst auf den Übergebenen Parametern ein Hashkey gebildet. Dieser wird dann an den Namen des Caches angehängt, um später zu überprüfen, ob diese Anfrage bereits gecached wurde. Wenn dies der Fall ist, dann wird der Cache zurückgegeben und ansonsten wird der Cache neu geschrieben mit der parent::find Methode. Wie das genauer aussieht zeige ich nun wie folgt:

$tag = isset($this->name) ? '_' . $this->name : 'appmodel';
$paramsHash = md5(serialize($params));
$version = (int) Cache::read($tag, $this->cachename);
$fullTag = $tag . '_' . $type . '_' . $paramsHash;
if ($result = Cache::read($fullTag, $this->cachename)) {
  if ($result['version'] == $version) {
    return $result['data'];
  }
}
$result = array(
  'version' => $version,
  'data' => parent::find($type, $params)
);
Cache::write($fullTag, $result, $this->cachename);
Cache::write($tag, $version, $this->cachename);
return $result['data'];

Nun ist es möglich, wenn man z.B. den Index der Blogs abrufen lässt, dieser in die Cachegruppe "blogs" geschrieben wird und wenn ein View aufgerufen wird, der einzelne View als "blogid123" gespeichert wird. Wenn nun also dann ein neuer Blogeintrag geschrieben wird, wird nur die Cachegruppe "blogs" invalidiert und die einzelnen BlogIDs sind weiterhin gecached.

Man kann dies natürlich noch in die afterSave mit einbauen, aber dies sollte nun erstmal reichen, um euch einen kleinen Einblick darauf zu gewähren, wie ich mit CakePHP Cachegroups das Caching für meine kommende Seite realisieren werde. Verbesserungsvorschläge sind hier natürlich auch gerne gesehen und ich hoffe ich kann mich mit 1-2 Leuten austauschen, die schon ein wenig mehr Erfahrung mit CakePHP Cachegroups haben. Ich bin da leider noch recht neu auf dem Gebiet.

Hier nochmal der Aufruf des Blog Indexes mit Caching und einer Blog View mit Caching:

public function index() {
  $this->Blog->setCache(true, 'blogs');
}

public function view($id = null) {
  if ((int) $id > 0) {
    $this->Blog->setCache(true, 'blog_id_' . $id);
  }
}

Ich hoffe, dass alles soweit gut verständlich war und ich denke mal, dass die nächsten Blogeinträge auch entweder über dieses Thema sein werden, sofern erwünscht. Lasst mir doch einen Kommentar da, wenn dies euch geholfen hat oder Verbesserungsvorschläge für die CakePHP Cachegroups. Euer Mark