viernes, 21 de diciembre de 2007

Optimizando Consultas en el modelo. Segunda Parte

Continuando con el punto anterior de como optimizar un poco las consultas en el modelo (y retomando el blog, que lo he tenido, por diversos temas, un poco abandonado) a continuación mostraré como extender el ejemplo anterior para poder utilizarlo con un paginador de un modulo bien sea creado de manera manual o con el admin-generator.

Symfony utiliza un clase llamada sfPropelPager para crear un objeto que nos permita "paginar" los registros generados a partir de una consulta a la BdD. Al instanciar un objeto a partir de dicha clase, se requieren especificar un par de parámetros al mismo, uno es el nombre del Modelo con que trabajará y el segundo es el tamaño de la pagina (cantidad de registros a mostrar por página). Para poder operar correctamente, el paginador necesita realizar 2 consultas a la BdD, una para contar la cantidad de registros que cumplen con el criterio de consulta ( en caso de que especifique alguna condición ) y la segunda para traer los registros de la pagina solicitada.

Retomando el ejemplo que utilizamos en el post anterior vamos a optimizar el paginador de un módulo de personas, que asumiremos fue generado con el admin-generator, para que muestre la información tal cual como la retorna el método getList() del la clase PersonaPeer.

Para esto es necesario crear un método en la clase PersonaPeer que "cuente" los registros con las mismas condiciones que utiliza el método getList(). A continuación se muestra la clase PersonaPeer:



<?php

/**
* Subclass for performing query and update operations on the 'persona' table.
*
*
*
* @package lib.model
*/

class PersonaPeer extends BasePersonaPeer
{
public static function getList(Criteria $criteria)
{
$personas = array();
// Clonamos el objeto, para evitar modificar el objeto original
$criteria = clone $criteria;
// Eliminanos las columnas de selección en caso de que esten definidas
$criteria->clearSelectColumns();
// Agregamos las columnas de las tablas que queremos recuperar
$criteria->addSelectColumn(PersonaPeer::ID_PERSONA );
$criteria->addSelectColumn(PersonaPeer::NOM_PERSONA );
$criteria->addSelectColumn(PaisPeer::NOM_PAIS );
$criteria->addSelectColumn(EstadoPeer::NOM_ESTADO );
$criteria->addSelectColumn(MunicipioPeer::NOM_MUNICIPIO );
// Agregamos los Joins entre las distintas tablas
PersonaPeer::addConditionsList($criteria);
//Recuperamos los registros y generamos el arreglo de hashes
$rs = PersonaPeer::doSelectRS($criteria);
while ($rs->next())
{
$persona['id'] = $rs->getInt(1);
$persona['nombre'] = $rs->getString(2);
$persona['pais'] = $rs->getString(3);
$persona['estado'] = $rs->getString(4);
$persona['municipio'] = $rs->getString(5);
$personas[] = $persona;
}
return $personas;
}

public static function getCountList(Criteria $criteria)
{
$criteria = clone $criteria;
$criteria->clearSelectColumns()->clearOrderByColumns();
$criteria->addSelectColumn(PersonaPeer::COUNT);
PersonaPeer::addConditionsList($criteria);
$rs = PersonaPeer::doSelectRS($criteria, $con);
if ($rs->next()) {
return $rs->getInt(1);
} else {
return 0;
}
}

protected static function addConditionsList(Criteria $criteria)
{
$criteria->addJoin(PersonaPeer::ID_PAIS ,PaisPeer::ID_PAIS,Criteria::LEFT_JOIN );
$criteria->addJoin(PersonaPeer::ID_ESTADO , EstadoPeer::ID_ESTADO,Criteria::LEFT_JOIN );
$criteria->addJoin(PersonaPeer::ID_MUNICIPIO, MunicipioPeer::ID_MUNICIPIO );
}
}




Como vemos, tenemos un método que llamamos getCountList() que permite realizar el conteo de los registros con las mismas condiciones que el método getList(); y aplicando la premisa DRY (Don't Repeat Yourself) las condiciones de filtrado las colocamos en un método protegido que es llamado desde getCountList() y getList().

Ahora que tenemos los 2 métodos necesarios para crear un paginador, vamos a proceder a actualizar nuestro módulo autogenerado, reescribiendo la menor cantidad de código posible.

Primero, vamos a modificar el generator.yml del módulo persona para indicar que se van a utilizar un método de conteo y de recuperación de registros específicos (Por defecto los objetos creados a partir de la clase sfPropelPager utilizan los métodos doCount y doSelect de la clase Peer para realizar esta tarea):


generator:
class: sfPropelAdminGenerator
param:
model_class: Persona
theme: default
list:
display: [id_persona, nom_persona, _pais, _estado, _municipio]
peer_method: getList
peer_count_method: getCountList


En la opción display colocamos el nombre de los campos que queremos mostrar teniendo en cuenta lo siguiente: Los campos que no pertenecen directamente a la tabla del modelo, como el nombre de pais, del estado, entre otros, deben especificarse como parciales, estos para que no se genere link de ordenamiento para dichos campos ( se puede ajustar el modulo para que el paginador acepte ordenamiento por campos que no pertenecen a la tabla del modelo, pero eso puede ser tema para otro post ;) ).
Los otras 2 opciones, peer_method y peer_count_method nos permiten especificar que métodos del post queremos que sean usados por el paginador para realizar la consulta.

Ahora, algo muy importante, por defecto el generator asume que los que le retorna el método de recuperación de registro, en nuestro caso el getList(), es un arreglo de objetos, pero en nuestro caso este método retorna un arreglo de hashes. Que hacemos?, pues la manera más sencilla de hacerlo, y que viene a demostrar la eficiencia del código autogenerado por symfony, es reescribir un parcial, específicamente el que se llama _list_td_tabular.php (en caso de que se especifique que la información sa va a mostrar apilada, debe reescribirse el parcial _list_td_stacked.php). Creamos dicho parcial en la carpeta templates de muestro módulo y, siguiendo con nuestro ejejmplo, le colocamos el siguiente ćodigo:



<td><?php echo link_to($persona['id'] ? $persona['id'] : __('-'), 'persona/edit?id_persona='.$persona['id']) ?></td>
<td><?php echo $persona['nombre'] ?></td>
<td><?php echo $persona['pais'] ?></td>
<td><?php echo $persona['estado'] ?></td>
<td><?php echo $persona['municipio'] ?></td>




Como vemos, con redefinir este parcial de código autogenerado, podemos utilizar el arreglo de hashes que retorna el método getList(), en vez del objeto esperado. Esto incluso nos da la ventaja que no tenemos que definir los parciales que indicamos el la opción display del generator.

Con un poco de creatividad y revisando el código autogenerado, podemos adaptar nuestro módulo para que filtre y ordene por campos del modelo de Pais, Estado o Municipio.