Cache Doctrine avec SQLite

Doctrine propose de stocker en cache soit la requête DQL compilée (évite de générer la requête SQL à chaque appel)  ou bien carrément le résultat de la requête (évite de faire travailler la BDD + échanges réseau).

L’implémentation permet de se brancher sur n’importe quelle système de stockage : Memcache, APC, base de données, fichier, session, mémoire, choisissez celui qui vous convient.

Le dernier, en mémoire servira plutôt sur une page répétant la même requête N fois, puisque le cache est vidé dès lors que la page a fini d’être chargée.

Pour mon projet, j’ai choisi SQLite, qui s’approche beaucoup d’un cache « fichier » mais avec tous les avantages de cette base très performante et ultra-rapide.

Comment faire ?

Côté configuration :

Doctrine propose une interface (Doctrine_Cache_Interface) et une classe abstraite (Doctrine_Cache_Driver) pour son cache, et plusieurs implémentations concrètes possibles :

APC, Array, Db, Memcache, Xcache.

Pour utiliser SQLite, il faut utiliser la classe Doctrine_Cache_Db.

Elle attend en constructeur, une connection à la base de données, qui aura été initialisée via le Doctrine_Manager + un DSN.

Au final, voici la méthode configureDoctrine à ajouter à votre configuration frontend (si c’est bien le nom de votre application) :

Fichier app/frontend/config/frontendConfiguration.class.php :

/**
* Configuration de Doctrine.
* Utilisé pour initialiser le cache de requête et de résultat.
* @param Doctrine_Manager $manager
* @author Etienne VOILLIOT
*/
public function configureDoctrine(Doctrine_Manager $manager)
{
    $manager = Doctrine_Manager::getInstance();

    if (sfConfig::get('app_doctrine_cache') == 'on')
    {
        $cacheConn = Doctrine_Manager::connection(new PDO( sfConfig::get('app_doctrine_cache_dsn') ));
        $cacheDriver = new Doctrine_Cache_Db(array('connection' => $cacheConn, 'tableName' =>'cache'));

/*
// Initialisation du cache Doctrine APC (pour info, je vous le laisse)
$cacheDriver = new Doctrine_Cache_Apc();

$manager->setAttribute(Doctrine::ATTR_QUERY_CACHE, $cacheDriver);
$manager->setAttribute(Doctrine::ATTR_QUERY_CACHE_LIFESPAN, sfConfig::get('app_cache_lifetime'));
$manager->setAttribute(Doctrine::ATTR_RESULT_CACHE, $cacheDriver);
$manager->setAttribute(Doctrine::ATTR_RESULT_CACHE_LIFESPAN, sfConfig::get('app_cache_lifetime'));
*/

        // LE 1er coup, pour créer la table //
        // $timer = new sfTimer();
        // On utilise une requête manuelle, car celle de doctrine générée par le createTable, n'inclut pas le IF NOT EXISTS, qui m'arrange !
        $cacheConn->exec('CREATE TABLE IF NOT EXISTS cache (id VARCHAR(255), data LONGBLOB, expire DATETIME, PRIMARY KEY(id))');

        // $cacheDriver->createTable();
        // echo 'time='.$timer->addTime();
    }
    else
    {
        // POUR DESACTIVER LE CACHE :
        $cacheDriver = new Doctrine_Cache_None();
    }

    $manager->setAttribute(Doctrine::ATTR_QUERY_CACHE, $cacheDriver);
    $manager->setAttribute(Doctrine::ATTR_QUERY_CACHE_LIFESPAN, sfConfig::get('app_doctrine_cache_lifetime', 3600));
    $manager->setAttribute(Doctrine::ATTR_RESULT_CACHE, $cacheDriver);
    $manager->setAttribute(Doctrine::ATTR_RESULT_CACHE_LIFESPAN, sfConfig::get('app_doctrine_cache_lifetime', 3600));
}

Le code suivant repose sur le fichier de configuration app.yml, qui contiendra les entrées suivantes :

doctrine_cache: on
doctrine_cache_lifetime: 1200
doctrine_cache_dsn: "sqlite:%sf_root_dir%/cache/cache.db"

J’aurais préféré le stocker dans le database.yml, mais je ne sais pas faire…

J’ai choisi de stocker la base dans le répertoire cache du projet. Libre à vous de la mettre n’importe où, mais en local, c’est mieux, sinon l’effet bénéfique du cache sera limité… Et dans le répertoire de cache, ça permet d’avoir les droits d’écriture, et de pouvoir la supprimer facilement (encore que je rencontre des problèmes avec la commande clear-cache, qui refuse de supprimer la base sqlite car lockée en écriture, mais c’est cosmétique…)

Remarquez que pour rendre le système transparent et éviter d’avoir à créer une base sqlite manuellement, j’ai mis la requête de création de la table dans la configuration. C’est pas la plus performante des solutions, mais franchement négligeable quand on regarde le temps utilisé pour faire cette requête.

Côté code dans les DAO

Il suffit d’ajouter en bout de requête DQL :

–          La fontion ->useResultCache() pour stocker le résultat de la requête en cache

–          La fonction ->useQueryCache() pour stocker la requête SQL en cache

Le système pourrait être amélioré, paufiné, mais en production, ça fonctionne déjà très bien et limité grandement le nb de requêtes. Par contre, il faut comme d’habitude avec le cache, faire attention aux mises à jour des tables impliquées dans les requêtes dont le résultat est caché, car on peut parfois s’étonner que telle chose n’apparaisse pas, c’est souvent car le cache n’est pas remis à zéro.

Pour notre projet, j’ai ajouté un lien en back-office pour vider le cache (suppression tout bonnement du fichier SQLite).