Symfony surcharge BasePeer::doSelect()

ViPHP
fab
ViPHP | 2657 Messages

10 janv. 2009, 00:11

Bonjour tlm :)

Symfony c'est bien c'est beau et tout et tout... Mais voilà que j'ai un petit problème. J'ai une table de plus de 400000 enregistrements avec bien sur d'autres tables qui ont des foreignKey pointant vers elle.

Je voudrais donc mettre ses données en cache ( la forme m'importe peu, mais surement tableau objets propel serializés ), principalement pour l'interface d'admin ou les selects formés à partir de cette table sont très lourds. Je pourrais tout aussi bien désactiver l'affichage du select mais c'est un choix que je n'ai pas.

Le but est donc intercepté au niveau du modèle les "requètes" demandant l'affichage de TOUS les enregistrements.
Soit
 ModelePeer::doSelect(new Criteria()); 
ou equivalent.

Donc j'ai pensé à ça
lib/model/ModelePeer.php
class ModelePeer.php extends BaseModelePeer
{
  	public static function doSelect(Criteria $criteria, $con = null)
	{
		$c = new Criteria();
		if($c == $criteria) {
			// get cache
		} else {
			parent::doSelect($criteria,$con);
		}
	}
}
Qu'en pensez vous?
Seul l'intelligent a le pouvoir de se trouver con
try { work(); } catch(FlemmeExeption $e) { sleep(84600); }

Administrateur PHPfrance
Administrateur PHPfrance | 3131 Messages

14 janv. 2009, 12:28

Déjà tu peux intercaler très proprement une classe entre "BasePeer" et "MonModelePeer", en utilisant l'option "baseClass" dans ton schema.yml (au même endroit que "phpName") :

Code : Tout sélectionner

mon_modele: _attributes: { basePeer: MaBasePeer }
Quand Propel génèrera le modèle, on aura alors :
- MonModelePeer extends BaseMonModelePeer extends MaBasePeer extends BasePeer
- MonModele extends BaseMonModele extends BaseObject (il est également possible d'utiliser l'option "baseObject" pour intercaler une classe entre "BaseMonModele" et "BaseObject")

Bref, ça c'est déjà pour te permettre d'étendre BasePeer dans ton modèle sans avoir à faire du copier-coller dans chaque classe de ton modèle ;)
Tu auras par exemple une classe "CachedSelectPeer" que tu appliques comme "basePeer" des classes du modèle que tu veux mettre en cache.




Ensuite, concernant le cache en lui-même, on peut faire quelque chose d'assez générique :
class CachedSelectPeer extends BasePeer
{
  
  /**
   * Mise en cache des résultats de requête
   * 
   */
  protected static $cache = null;
    
  /**
   * Effectue la requête, mise en cache si possible
   *
   * @param      Criteria $criteria A Criteria.
   * @param      Connection $con A connection to use.
   * @return     ResultSet The resultset.
   * @throws     PropelException
   * @see        createSelectSql()
   */
  public static function doSelect(Criteria $criteria, $con = null)
  {
    // Chercher dans le cache
    if (!($result = self::cachedResult($criteria, $con))) {
      // Pas trouvé : on exécute la requête et on met en cache
      $result = parent::doSelect($criteria, $con);
      self::cacheResult($criteria, $result, $con);      
    }
    
    return $result;
  }
  
  /**
   * Retourne le résultat mis en cache de la requête
   *
   * @param Criteria $criteria
   * @param Connection  $con
   * @return ResultSet
   */
  protected static function cachedResult(Criteria $criteria, $con = null)
  {
    if (!self::$cache) {
      self::initCache();
    }
    
    // à implémenter
  } 
  
  /**
   * Met en cache un résultat
   *
   * @param Criteria $criteria
   * @param ResultSet $result
   * @param Connection  $con
   * @param int $timeout Expiration du cache (secondes)
   */
  protected static function cacheResult(Criteria $criteria, ResultSet $result, $con = null, $timeout = 3600)
  {
    if (!self::$cache) {
      self::initCache();
    }
    
    // à implémenter
  } 
  
  /**
   * Initialise le cache
   *
   */
  protected static function initCache()
  {
    // à implémenter
  }
  
}



Les éléments "à implémenter" dépendent du cache que tu veux mettre en place :
- un cache basé sur un vulgaire tableau limité à l'exécution en cours (avec self::$cache qui sera un array)
- un cache "persistant" basé sur l'API de sfCache (sfFileCache et serialize peuvent fonctionner ensemble, ça aura l'avantage d'être persistant mais il faut vraiment benchmarker parce que ce n'est pas si sûr qu'on y gagne en perfs), avec la possibilité d'utiliser des plugins comme sfMemCached qui déchire pas mal ;)


Si on part sur un simple tableau, il faudra penser que Criteria est un objet, qu'on ne peut pas se baser sur son "toString" pour avoir une unicité, et donc qu'on ne peut pas avoir un joli tableau associatif bien performant : il faudra à chaque fois le parcourir :(

initCache :
self::$cache = array();
cachedResult :
foreach (self::$cache as $cache) {
  if ($criteria->equals($cache['criteria'])) {
    return $cache['result'];
  }
}

return false;
cacheResult :
self::$cache[] = array(
  'criteria' => $criteria,
  'result' => $result,
);



Voilà un exemple d'implémentation de ce que tu veux faire, mais je pense que c'est inutilisable en l'état pour cause de performances qui seront probablement plus dégradées que sans le cache ;)


Note 1 : à ce stade on met en cache les "ResultSet", il peut effectivement être plus intéressant pour toi de mettre en cache les objets Propel déjà "hydratés". Dans ce cas comme cette étape se fait dans "BaseMonModele" tu seras effectivement obligé d'ajouter manuellement une méthode dans "MonModele".

Note 2 : un select de 40'000 éléments, ce n'est pas tant que ce soit lourd à charger côté serveur, mais c'est surtout inutilisable côté client. Du coup, le cache est-il vraiment la solution ?

Eléphant du PHP | 291 Messages

14 janv. 2009, 13:14

Tiens c'est marrant, j'ai eu des problématiques assez similaires, mais je travaille maintenant uniquement avec des resultset, pour éviter d'avoir des tableaux trop consommateurs de mémoire.

Administrateur PHPfrance
Administrateur PHPfrance | 3131 Messages

14 janv. 2009, 13:16

Quel intérêt d'utiliser Propel pour ne travailler qu'avec les ResultSet ?
Il vaut mieux dans ce cas utiliser directement PDO tu gagnerais beaucoup en performance vu que de toute façon en faisant ça tu fais une croix sur l'intérêt principal de Propel (abstraction des données de la tables en classe et possibilité d'overloader les getters/setters).

Eléphant du PHP | 291 Messages

14 janv. 2009, 13:18

C'est ce que je fais... du moins pour les entités avec lesquelles j'ai vraiment besoin de travailler avec les resultSet (selection + affichage de plusieurs dizaines de milliers de données)

ViPHP
fab
ViPHP | 2657 Messages

14 janv. 2009, 14:37

Merci beaucoup pour ta réponse naholyr :)

Très intéressant mais dans ta Note 2 tu soulignes parfaitement le problème que j'ai finalement rencontré.
Le <select> formé est légèrement trop gros, en terme de poids "pur" ça représente 5 - 6 mo donc niveau temps de chargement c'est pas top du tout, le navigateur met aussi son petit temps pour mettre en page tout ça. Et si l'on considère le côté "pratique" de la chose, c'est aussi vrai qu'il est quasi nul :)

Je pense donc m'orienter vers 2 choses
- filters propel : Tenter l'instauration d'un filtre "custom"
- ajout ( propel auto admin ) : Remplacer le <select> par un champ custom


Dans les deux cas ça me donner l'occasion de fouiner encore un peu plus, car dans la doc de base ... :D
Seul l'intelligent a le pouvoir de se trouver con
try { work(); } catch(FlemmeExeption $e) { sleep(84600); }

Eléphant du PHP | 291 Messages

14 janv. 2009, 15:01

... elle est "de base" , comme tu dis :twisted:

Administrateur PHPfrance
Administrateur PHPfrance | 3131 Messages

14 janv. 2009, 15:25

Il est assez aisé pour toi de créer un nouveau type de champ dans l'admin generator, il suffit :
- d'avoir (ou de créer) un helper inclus dans lequel on va ajouter une fonction à soi
- de s'inspirer de la fonction "object_select_tag" et de faire une fonction "object_XYZ" qui corresponde à ce que tu veux faire
- de spécifier "type=XYZ" dans ton generator.yml

Dans ton cas tu voudras soit faire une champ avec auto-complétion Ajax (dans ce cas il faudra aussi ajouter une méthode dans ton actions.class.php pour faire la requête Ajax) ou alors simplement générer un gros tableau JSON à partir des données et faire ton filtrage à la volée en JS. À voir ce qui marche le mieux question perf ;)

ViPHP
fab
ViPHP | 2657 Messages

14 janv. 2009, 16:50

Alors pour les filters j'ai trouvé tout bien comme il faut :) Mais en utilisant les "partials"
generator.yml

Code : Tout sélectionner

fields: cities_id: { name: Ville } code: { name: Cp } cities: { name: Ville } list: title: Liste des codes postaux display: [ =code, cities] filters: [ code, _cities]
templates/_cities.php ( j'épargne tout le coté d'ajax bien spécifique a mon appli
<?php echo input_tag('filters[customVille]')?>
actions.class.php
	protected function addFiltersCriteria($c) // surcharge de la méthode autogénérée
	{	
            // on utilise (array) $this->filters rempli comme il faut par la class autogénérée
	    if(isset($this->filters['customVille']) && $this->filters['customVille'] !== '') {
			$c->add(ZipsPeer::CITIES_ID,$this->filters['customVille']);
		}
	
		parent::addFiltersCriteria($c); // on appel la "method" de la classe parent pour gérer les autres filres ( non custom )
	}


Mais par contre je bloque complètement sur ta méthode Naho, je vois pas du tout comment faire :s
J'ai tenté de faire un - cities_id: { type: XNAB} avec dans le object helper avec exactement le meme contenu que object_select_tag sauf bien sur le nom :)
Et ça dans le but de voir comment de comporter les class autogénéré, mais il y a des erreurs partout dedans :'(
Seul l'intelligent a le pouvoir de se trouver con
try { work(); } catch(FlemmeExeption $e) { sleep(84600); }

Administrateur PHPfrance
Administrateur PHPfrance | 3131 Messages

14 janv. 2009, 22:20

Ben normalement tu crées une fonction "object_XNAB" qui soit un copier-coller de "object_select_tag" dans un helper chargé, et "type: XNAB" marchera nickel (en mode edit uniquement, en mode list il n'est pas possible de surcharger le type affiché dans l'admin generator "de base").

Sinon je vois que tu surcharges la méthode "addFiltersCriteria", ça me semble étrange vu qu'on est dans le mode edit ? Je vois mal comment ça se concrétise au niveau de l'interface ton bignou :o

ViPHP
fab
ViPHP | 2657 Messages

14 janv. 2009, 22:36

Pour la surcharge la méthode "addFiltersCriteria" c'était pour l'action "list" en filters j'avais aussi ma clef étrangère donc le <select> de 15 km :)

Alors voici mon generator.yml

Code : Tout sélectionner

edit: title: Edition d'un code postal display: - code - cities_id: { type: XNAB }
Voici ce qui se trouve dans mon lib/symfony/ObjectHelper.php
( je sais que je dois pas modifier les fichiers là directement mais plutot placer un nouveau fichier dans lib/helper/ mais c'est juste pour tester
function object_XNAB($object, $method, $options = array(), $default_value = null)
{
  $options = _parse_attributes($options);

  $related_class = _get_option($options, 'related_class', false);
  if (false === $related_class && preg_match('/^get(.+?)Id$/', $method, $match))
  {
    $related_class = $match[1];
  }

  $peer_method = _get_option($options, 'peer_method');

  $text_method = _get_option($options, 'text_method');

  $key_method = _get_option($options, 'key_method', 'getPrimaryKey');

  $select_options = _get_options_from_objects(sfContext::getInstance()->retrieveObjects($related_class, $peer_method), $text_method, $key_method);

  if ($value = _get_option($options, 'include_custom'))
  {
    $select_options = array('' => $value) + $select_options;
  }
  else if (_get_option($options, 'include_title'))
  {
    $select_options = array('' => '-- '._convert_method_to_name($method, $options).' --') + $select_options;
  }
  else if (_get_option($options, 'include_blank'))
  {
    $select_options = array('' => '') + $select_options;
  }

  if (is_object($object))
  {
    $value = _get_object_value($object, $method, $default_value);
  }
  else
  {
    $value = $object;
  }

  $option_tags = options_for_select($select_options, $value, $options);

  return select_tag(_convert_method_to_name($method, $options), $option_tags, $options);
}

Je me retrouve avec plein d'erreurs du genre
<br />
<b>Notice</b>: Undefined offset: 0 in <b>/Users/fabienmeynard/Sites/sandbox/lib/symfony/generator/sfAdminGenerator.class.php</b> on line <b>416</b><br />
<br />
<b>Notice</b>: Array to string conversion in <b>/Users/fabienmeynard/Sites/sandbox/lib/symfony/util/sfToolkit.class.php</b> on line <b>395</b><br />
<br />
<b>Notice</b>: Array to string conversion in <b>/Users/fabienmeynard/Sites/sandbox/lib/symfony/util/sfInflector.class.php</b> on line <b>51</b><br />
dans mon
cache/backend/dev/modules/autoZips/actions/actions.class.php

Et quand j'enlève le { type: XNAB } j'ai plus aucune erreur donc il doit avoir une étape que j'ai raté quelque part :'(
Seul l'intelligent a le pouvoir de se trouver con
try { work(); } catch(FlemmeExeption $e) { sleep(84600); }

Administrateur PHPfrance
Administrateur PHPfrance | 3131 Messages

15 janv. 2009, 09:18

Hin hin, j'avais pas compris que c'était dans les filters. Ce que je te disais c'est pour les champs en mode edit, pour les filters je ne suis pas sûr je n'ai pas eu de cas "général" du coup j'ai toujours fait avec un partial comme toi ici.

Je regarde parce que tout de même c'est intéressant de savoir le faire ;)

ViPHP
fab
ViPHP | 2657 Messages

15 janv. 2009, 13:34

J'avais les deux problématiques en fait, le premier ( filters ) j'ai réussi avec les partials et le deuxième ( le select dans le l'action edit ) j'essaye avec ta méthode mais sans succès :D
Seul l'intelligent a le pouvoir de se trouver con
try { work(); } catch(FlemmeExeption $e) { sleep(84600); }