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.