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.

viernes, 28 de septiembre de 2007

Crear listas dependientes con Ajax en Symfony. Segunda Parte.

En el penúltimo post abordé el tema de como manejar una lista dependiente usando Ajax. El ejemplo planteado es muy efectivo cuando tenemos una lista que depende de otra. Pero, que pasa cuando existe otra lista que a su vez dependa de esta última lista en cuestión, por ejemplo, tener la lista de municipios, que dependa de una lista de estados, que a su vez dependa de una lista de paises. A continuación veremos como con unas pequeñas variaciones del dicho ejemplo podemos conseguir esto, e incluso, poder hacer listas de n niveles de dependencia.

La Dirección de una Persona.

Para el ejemplo vamos a suponer que tenemos un modelo con 4 tablas; país, estado, municipio y persona, donde el registro de la persona contiene un referencia a las otras 3 tablas.
Para cada una de ellas vamos a crear un módulo administrativo.

symfony propel-init-admin test pais Pais
symfony propel-init-admin test estado Estado
symfony propel-init-admin test municipio Municipio
symfony propel-init-admin test persona Persona

  • Los componentes
Aprovechando la potencia que ofrecen los componentes, vamos a crear 3 de ellos, uno para país, otro para el estado y otro para el municipio. Los componentes para el Estado y Municipio reciben hasta 3 parámetros; el nombre que tendrá el objeto select a nivel de formulario, el id seleccionado por defecto, y el id del objeto padre para filtrar la búsqueda ( por ejemplo, el componente del municipio recibe el id del estado para filtrar solo los municipios que pertenecen a ese estado). En el caso del país, recibe solo 2 parámetros, el nombre del objeto select y el id del país seleccionado por defecto (El componente del municipio es el mismo que se mostró en el post anterior).
A continuación se muestra la parte de la lógica de negocio del componente de país:

<?php
class paisComponents extends sfComponents
{
public function executeSelectAll()
{
$this->paises = PaisPeer::doSelect(New Criteria());
}
}

Y acá el parcial _selectAll.php:

<?php echo select_tag($nombre,options_for_select(array(''=>'Seleccione')+
_get_options_from_objects($paises),
isset($id_pais)?$id_pais:''),array());?>

Ahora el componente en el módulo estado:

<?php
class estadoComponents extends sfComponents
{
public function executeSelectByPais()
{
$this->estados = EstadoPeer::doSelectByPais($this->id_pais);
}
}

El parcial _selectByPais.php del componente.

<?php
echo select_tag($nombre,options_for_select(array(''=>'Seleccione')+
_get_options_from_objects($estados),
isset($id_estado)?$id_estado:''),array());?>

  • Las Acciones que se llamarán vía Ajax.

Para el ejemplo que nos compete necesitamos tener 2 acciones a ejecutar vía Ajax: Una que se ejecutará al seleccionar un país, y que debe actualizar la lista de estados, y otra que se ejecutará al seleccionar un estado y que debe actualizar la lista de municipios.
Hay un punto muy importante a tener en consideración. Como vimos en el post previo relacionado con este caso, a cada lista de la que dependa otra lista le tenemos que crear un observador. En este caso tenemos que crear un observador al campo que selecciona el país y otro al que selecciona el estado. En este punto en donde la cosas se complican un poco. Haciendo un poco de memoria debemos recordar que no podemos actualizar el objeto select de manera directa, esto por un bug presente en Internet Explorer, y que nos obligo a colocar el objeto select dentro de un span y "recrearlo" completamente vía Ajax. Volviendo al ejemplo, creamos un acción llamada listByPais en el módulo de Estado, y una acción llamada listByEstado en el módulo municipio, y siguiendo el ejemplo del post previo creamos la vistas respectivas, donde se llaman a los componentes ya descritos; procedemos a crear los parciales _id_pais.php , _id_estado.php y _id_municipio.php en el módulo de personas, donde se llaman a los componentes de cada campo y colocando además un observador al campo país y otro al campo estado. Editamos el generator.yml especificando los parciales y ejecutamos desde el navegador el modulo persona, llamando la acción create. Seleccionamos el país, y la lista de estados se actualiza perfectamente, pero cuando seleccionamos un estado, la lista de municipios no se actualiza. Si tienes una herramienta como firebug instalada observarás que la llamada a la acción municipio/listByEstado no se ejecutó. Por qué?, porque al seleccionar el país, el objeto estado fue literalmente "borrado y creado nuevamente", por lo que el observador "pierde la conexión" con dicho objeto. Que hacemos?, como solventamos esta falla?. Personalmente conozco 4 formas de resolver este problema en Symfony, pero para nuestro ejemplo voy a explicar la que considero más al "estilo symfony". Para este caso la acción estado/listByPais no nos sirve, esta solamente es útil para casos donde no exista una lista que dependa de la que el genera. Procedemos entonces a crear en el modelo persona una nueva acción llamada listEstadosByPais:

<?php
class personaActions extends autopersonaActions
{
public function executeListEstadosByPais()
{
}
}


Ahora en la vista listEstadosByPaisSuccess.php vamos a llamar al componente selectByPais del módulo estado, y adicionalmente, vamos a crear nuevamente el observador a dicho campo.


<?php use_helper('Object','Javascript') ?>

<?php include_component('estado','selectByPais',array(
'id_pais' => $sf_request->getParameter('id_pais'),
'nombre' => 'persona[id_estado]',
));
?>
<?php echo observe_field('persona_id_estado',array(
'update' => 'municipio',
'url' => 'municipio/listByEstado',
'with' => "'id_estado=' + value + '&nombre=persona[id_municipio]'",
))
?>

ahora vamos al parcial _id_pais.php y lo vamos a cambiar de la siguiente manera:

<?php include_component('pais','selectAll',array(
'id_pais'=>$persona->getIdPais(),
'nombre'=>'persona[id_pais]',
));?>
<?php echo observe_field('persona_id_pais',array(
'update' => 'estado',
'url' => 'persona/listEstadosByPais',
'with' => "'id_pais=' + value",
'script' => true,
)) ?>

Como vemos, ahora el observador llamará a la acción que acabamos de crear. Aparte le configuramos la acción script en true. Esto es para que cuando se haga el llamado a la acción persona/listEstadosByPais, el código javascript que crea el observador sea ejecutado.
Así, cuando ejecutemos nuevamente la acción create del módulo persona, al seleccionar un país, se regenerá la lista de estados, y se recrea el observador del mismo.
Anexo un link para descargar el proyecto completo, que incluye además un ejemplo de como utilizar listas dependientes para filtrar (en el módulo municipio, al listar, puede filtrarse por pais y estado).

En un próximo post estaré hablando de como hacer consultas complejas a nivel del modelo explotando las bondades del objeto criteria.