Sauvegarder les sessions en base de données

Pierre CLÉMENT 10 décembre 2011 2
Sauvegarder les sessions en base de données

Pourquoi faire ça ?

Il y a plusieurs raisons à chercher à stocker ses sessions en base de données :

  1. même si pour ma part je suis à l’abri de ça, la première raison peut-être si vous faites du load-balancing : par défaut, les sessions sont stockées dans des fichiers dans des sombres recoins du serveur. Prenons donc le cas suivant : votre utilisateur visite votre site, créé une session parce qu’il s’est par exemple loggé sur l’interface privée pour passer une commande de missiles nucléaires (en cherchant, il doit bien y avoir des sites de B2T – Buyer to Terrorist – autre que ebay), rempli bien son panier, et au moment de valider sa commande il est basculé sur l’autre serveur pour gérer l’affluence. Manque de chance, sa session n’est pas sur l’autre serveur, et votre utilisateur doit se reloger et refaire son panier.. Plutôt moyen. En ayant tout dans une seule base de données, le problème est réglé, vos terroristes sont content.
  2. plus abordable pour le petit développeur, si tout est en base de données, on peut en une requête SQL afficher dans l’interface d’admin le nombre d’utilisateurs connectés.
  3. de la même façon, on peut aller analyser plus simplement le comportement de ses visiteurs suivant ce que l’on stocke dans la variable session.
  4. suivant les quelques bench que j’ai pu voir ça et là, pour les sites à forte affluence les requêtes SQL sont plus rapides que les accès fichier au delà d’un certain seuil (info à prendre telle quelle, j’ai pas de graphique pour l’instant).
  5. parce que ça fait hype dans les soirées geeks de dire que tes sessions tu les stocks en BDD

Et c’est dur ?

Mais non l’ami, grâce à la fonction php session_set_save_handler, il suffit de définir quelle méthode appeler selon l’action. On peut donc faire des appels vers une BDD, mais aussi mettre les sessions dans le cache.
On remercie l’ami(e) Stalker qui a mit cet exemple sur php.net, pour stocker vos sessions en base de données MySQL (à vous de gérer pour PDO hein) :
CREATE TABLE `ws_sessions` (
  `session_id` varchar(255) binary NOT NULL default '',
  `session_expires` int(10) unsigned NOT NULL default '0',
  `session_data` text,
  PRIMARY KEY  (`session_id`)
) TYPE=InnoDB;
class session {      // session-lifetime      var $lifeTime;      // mysql-handle      var $dbHandle;      function open($savePath, $sessName) {         // get session-lifetime         $this--->lifeTime = get_cfg_var("session.gc_maxlifetime");
       // open database-connection
       $dbHandle = @mysql_connect("server","user","password");
       $dbSel = @mysql_select_db("database",$dbHandle);
       // return success
       if(!$dbHandle || !$dbSel)
           return false;
       $this->dbHandle = $dbHandle;
       return true;
    }
    function close() {
        $this->gc(ini_get('session.gc_maxlifetime'));
        // close database-connection
        return @mysql_close($this->dbHandle);
    }
    function read($sessID) {
        // fetch session-data
        $res = mysql_query("SELECT session_data AS d FROM ws_sessions
                            WHERE session_id = '$sessID'
                            AND session_expires > ".time(),$this->dbHandle);
        // return data or an empty string at failure
        if($row = mysql_fetch_assoc($res))
            return $row['d'];
        return "";
    }
    function write($sessID,$sessData) {
        // new session-expire-time
        $newExp = time() + $this->lifeTime;
        // is a session with this id in the database?
        $res = mysql_query("SELECT * FROM ws_sessions
                            WHERE session_id = '$sessID'",$this->dbHandle);
        // if yes,
        if(mysql_num_rows($res)) {
            // ...update session-data
            mysql_query("UPDATE ws_sessions
                         SET session_expires = '$newExp',
                         session_data = '$sessData'
                         WHERE session_id = '$sessID'",$this->dbHandle);
            // if something happened, return true
            if(mysql_affected_rows($this->dbHandle))
                return true;
        }
        // if no session-data was found,
        else {
            // create a new row
            mysql_query("INSERT INTO ws_sessions (
                         session_id,
                         session_expires,
                         session_data)
                         VALUES(
                         '$sessID',
                         '$newExp',
                         '$sessData')",$this->dbHandle);
            // if row was created, return true
            if(mysql_affected_rows($this->dbHandle))
                return true;
        }
        // an unknown error occured
        return false;
    }
    function destroy($sessID) {
        // delete session-data
        mysql_query("DELETE FROM ws_sessions WHERE session_id = '$sessID'",$this->dbHandle);
        // if session was deleted, return true,
        if(mysql_affected_rows($this->dbHandle))
            return true;
        // ...else return false
        return false;
    }
    function gc($sessMaxLifeTime) {
        // delete old sessions
        mysql_query("DELETE FROM ws_sessions WHERE session_expires < ".time(),$this->dbHandle);
        // return affected rows
        return mysql_affected_rows($this->dbHandle);
    }
}
$session = new session();
session_set_save_handler(array(&$session,"open"),
                         array(&$session,"close"),
                         array(&$session,"read"),
                         array(&$session,"write"),
                         array(&$session,"destroy"),
                         array(&$session,"gc"));
session_start();
// etc...

Et avec Symfony, c’est gérable ?

Mais oui, et en plus c’est tout simple (merci à Alister Stead pour son billet là dessus) !

Création de la table

Il faut une table qui va nous stocker ça. Voici la table à rajouter dans votre schema.yml pour Doctrine :

Session:
  columns:
    id:
      type: string(32)
      primary: true
      notnull: true
    session_data:
      type: clob(65535)
      notnull: true
    session_time:
      type: integer
      notnull: true

On fait son doctrine:build-all et hop ça, c’est fait !

Configuration de Symfony

Il ne vous reste plus qu’à configurer Symfony pour utiliser cette table pour stocker les sessions. Ajoutez ceci dans la section all du fichier apps/***/config/factories.yml :

  storage:
    class: sfPDOSessionStorage
    param:
      db_table: session # Table où seront stockées les sessions
      db_id_col: id # Clé primaire
      db_data_col: session_data # Champ qui contiendra les données
      db_time_col: session_time # Champ qui contiendra la date de la sessions
      database: doctrine # Nom de la connection à la base de données à utiliser

Et voilà c’est fini, allez sur votre appli, assurez d’aller dans une section où les sessions sont utilisées et allez jeter un oeil à votre base de données ;)

Dernier truc sympa, vous pouvez aussi aisément séparer les sessions de vos appli frontend et backend : si par exemple la session de votre frontend ne sert qu’à enregistrer des infos sur vos visiteurs comme par exemple la dernière recherche effectuée, pourquoi polluer cette table avec les sessions des administrateurs du site ?

Have fnu coding!

Références

Geekos.fr vous recommande les articles suivants

2 Commentaires »

  1. Jérémy 10 décembre 2011 au 23 h 09 min - Reply

    En fonction de la taille des session (nom ou quantité des données stockés) on pourrait même envisager d’utiliser des tables HEAP plutôt qu’INODB. Et permettrait d’avoir des réponse très réactives au détriment d’une faible capacités de données

    Par contre, une chose à savoir et que je n’ai pas vu dans l’article, c’est que, contrairement à la gestion de session standard, session_set_save_handle ne gère pas nativement les locks c’est a vous de l’implémenter. Ce qui signifie qu’un utilisateur qui lance en simultané 2 scripts (appels ajax par exemple ou 2 onglets) peuvent potentiellement perdre des données, par exemple :
    - Appel A lit la session (a=1, b=2)
    - Appel B lit la session (a=1, b=2)
    - A modifie la session (a=5) et enregistre le tout (a=5, b=2)
    - B modifie la session (b=6) et enregistre le tout (a=1, b=6)
    -> on a perdu la modification du script A

    C’est a vous d’implémenter dans la partie open et close, et veiller a ce que la session ne reste pas verrouillée lorsqu’un script plante brutalement.

    Encore un point, les loadbalancer permettent généralement de lire les cookies pour router les requêtes d’un même utilisateur vers le même front. Mais il est vrai que dans le cas ou un front s’arrête, la session est perdu et l’utilisation des session en base est grandement bénéfique.

    • Pierre CLÉMENT
      Pierre CLÉMENT 11 décembre 2011 au 1 h 46 min - Reply

      Bonsoir,
      Merci pour ces précisions, je n’avais pas pensé au type de la table (bien qu’effectivement les benchs que j’ai pu voir – que je ne retrouve bien sûr plus – mettaient ceci en avant). Je ne promet rien, mais je vais essayer de faire un benchmark des différentes solutions.
      Le point que tu soulignes – le fait que le session_set_save_handle ne gère pas les locks – n’est pas dans l’article pour la simple et bonne raison que je l’ignorais, merci de l’explication !
      Enfin, je n’ai pas d’expérience sur le load balancing, juste quelques notions, alors merci aussi pour ta dernière précision :)

Laissez un message »