Mostrando entradas con la etiqueta criteria. Mostrar todas las entradas
Mostrando entradas con la etiqueta criteria. Mostrar todas las entradas

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.

martes, 16 de octubre de 2007

Optimizando consultas en el modelo.

Utilizar un ORM tienes sus ventajas, pero también sus desventajas. Manejar el acceso a registros específicos de la base de datos para hacer modificaciones o consultas resulta muy cómodo utilizando objetos, y la penalización en el rendimiento es prácticamente imperceptible. Pero que pasa cuando necesitamos realizar una consulta que involucra campos de varias tablas, y la cantidad de registros a recuperar son cientos o quizás miles, como por ejemplo, la consulta para un reporte. Es viable realizarlo creando un método que cree objetos "enlazados", similar a como lo hace los métodos doSelectJoinXXXX de las clases peer de los modelos base, pero si la cantidad de registros es muy grande los tiempos de respuestas serán demasiado altos, y probablemente la aplicación se quede sin memoria, dependiendo la configuración memory_limit de PHP.
Para solventar este escollo es preferible crear un método en el modelo que genere en vez de un arreglo de objetos, un arreglo de hashes, en el cual solo se retornen los valores que nos interesan.
Volvamos al ejemplo con que trabajamos en el post anterior. Supongamos que queremos crear, en nuestro módulo personas, un reporte de tipo listado, donde aparezca el nombre de la persona, el país, estado y municipio donde reside. En la mayoría de las recomendaciones que encontramos en internet con respecto a estos casos indican que debemos crear método donde se defina el query con un string, parsearle los campos, y ejecutarlo. Veamos un ejemplo para entenderlo mejor:

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

class PersonaPeer extends BasePersonaPeer
{
public static function getList()
{
$personas = array();
$con = Propel::getConnection();
$query = 'SELECT %s , %s, %s,
%s, %s
FROM %s
left join %s on %s = %s
left join %s on %s = %s
left join %s on %s = %s'
;

$query = sprintf($query, self::ID_PERSONA , self::NOM_PERSONA ,
PaisPeer::NOM_PAIS , EstadoPeer::NOM_ESTADO , MunicipioPeer::NOM_MUNICIPIO ,
self::TABLE_NAME,
PaisPeer::TABLE_NAME ,self::ID_PAIS , PaisPeer::ID_PAIS ,
EstadoPeer::TABLE_NAME , self::ID_ESTADO , EstadoPeer::ID_ESTADO ,
MunicipioPeer::TABLE_NAME , self::ID_MUNICIPIO , MunicipioPeer::ID_MUNICIPIO );

$stmt = $con->prepareStatement($query);
$rs = $stmt->executeQuery();
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;
}
}

Como vemos, este método del peer retorna un arreglo de hashes, cuya busqueda y armado es más rápido que armar un arreglo de objetos. Pero, que desventajas tiene?. La principal desventaja es que, a diferencias de otros métodos del peer, esto no nos permite pasarle condiciones de filtrado u ordenamiento a través de criteria. Para solventar esa deficiencia vamos a reescribir el mismo pero usando un objeto criteria, que se recibirá como parámetro:

<?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(self::ID_PERSONA );
$criteria->addSelectColumn(self::NOM_PERSONA );
$criteria->addSelectColumn(PaisPeer::NOM_PAIS );
$criteria->addSelectColumn(EstadoPeer::NOM_ESTADO );
$criteria->addSelectColumn(MunicipioPeer::NOM_MUNICIPIO );
// Agregamos los Joins entre las distintas tablas
$criteria->addJoin(self::ID_PAIS ,PaisPeer::ID_PAIS,Criteria::LEFT_JOIN );
$criteria->addJoin(self::ID_ESTADO , EstadoPeer::ID_ESTADO,Criteria::LEFT_JOIN );
$criteria->addJoin(self::ID_MUNICIPIO, MunicipioPeer::ID_MUNICIPIO );
//Recuperamos los registros y generamos el arreglo de hashes
$rs = self::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;
}
}


Aunque fue un poco más difícil de escribir, este método hace el mismo trabajo que el anterior, con la ventaja que recibe un objeto criteria, con lo cual podemos definir filtros por cualquier campo de las 4 tablas involucradas en la consulta ( Ejem: Obtener los datos de personas que viven en un estado específico, entre otros), lo cual hace este método más reusable. En un próximo post veremos como podemos utilizar este mismo método en la acción list autogenerada de nuestro módulo persona.