miércoles, 26 de diciembre de 2007

Trabajando con múltiples shemas de postgresql en Symfony.

El manejador de BdD postgres, tal véz el mejor motor de BdD libre que existe (que me disculpen los amante de MySQL ;-) ) tiene la posibilidad de trabajar con schemas, que es una manera muy interesante de organizar las distintas tablas de nuestra BdD. Para entender un poco lo que son los schemas en postgres acá les colocó un link a la página de Expresión Digital donde tienen una definición bastante sencilla de lo que son y como se implementan ( Es importante no confundir el concepto de schema de postgres con el concepto aplicado a los archivos de schema utilizado en Symfony).

Una debilidad que tiene Propel/Creole, el ORM utilizado en Symfony, es que no soporta trabajar con múltiples schemas de postgresql. Por defecto este asume que trabajará con el schema por defecto, llamado public. Què pasa si tenemos múltiples schemas en nuestra BdD en Postgresql, y queremos utilizar Symfony?, pues sencillamente con la instalación por defecto de Symfony esto no es viable.
Pero viable no quiere decir que sea imposible. Hace aproximadamente un año me tocó trabajar con una BdD en Postgres con varios schemas con Symfony, y al toparme con esta debilidad de Propel/Creole procedí a reescribir un par de clases de el core de dicho ORM para solventar dicho escollo. Después de 2 días de trabajo logré ajustar las siguiente clase de Creole:
.- PgSQLConnection: Ubicada en la carpeta vendor/creole/drivers/pgsql en las librerías de Symfony, se encarga de realizar la conexión a la BdD en base a un arreglo de parámetros de conexión. A esta clase le agregué una condición para que en caso de que se especifique el parámetro schema se ejecute la instrucción SET search_path TO en la BdD, indicando los schemas que se hayan especificado. Con ese simple cambio es posible especificar un dsn en el archivo database.yml como el que se presenta a continuación:

dev:
propel:
class: sfPropelPgDatabase
param:
dsn: pgsql://miusuario:miclave@miservidor/mibdd?schema='public','VENTAS'

De esta manera nuestra aplicación en Symfony trabajaría con las tablas tanto del schema public como VENTAS de la BdD indicada.

Otro punto importante es si nosotros escribimos manualmente el archivo schema.yml de Symfony o lo generamos a partir de nuestra BdD con el comando symfony propel-build-schema. En el caso de usar el comando propel-build-schema tenemos las misma restricción, y que por defecto generará el schema.yml exclusivamente del schema public de la BdD. Para poder generar el schema.yml con las tablas de varios schemas modifiqué la siguiente clase:

.- PgSQLDatabaseInfo: Ubicada en la carpeta vendor/creole/drivers/pgsql/metadata en las librerías de Symfony, es utilizada para realizar las consultas necesarias a la BdD para la generación del schema.yml. Realicé algunos ajustes al método initTables de dicha clase para recuperar la lista de tablas de varios schemas, en caso de que se hayan especificado. De esta manera se puede editar el archivo propel.ini de la siguiente manera para generar el shcema.yml con las tablas de los schemas public y VENTAS de la BdD:

propel.targetPackage = lib.model
propel.packageObjectModel = false
propel.project = miproyecto
propel.database = pgsql
propel.database.createUrl = pgsql://miservidor/
propel.database.url = pgsql://miusuario:miclave@miservidor/mibdd?schema='public','VENTAS'


Un problema que tiene este mecanismo es el de manejar tablas con el mismo nombre entre los schemas, así por ejemplo pueden tener una tabla llamada persona en el schema public y otra llamada persona en el schema VENTAS, a nivel de postgres esto es válido, pero en Symfony, esto nos traería inconvenientes. Es posible crear una aplicación en nuestro proyecto que trabaje solo con el schema public, y otra que trabaje solo con el schema VENTAS con este cambio que les indico, pero si tienen tablas con el mismo nombre es recomendable que utilizen la propiedad package y/o phpname en el schema.yml para indicar que las clases de dichos schemas se generen en directorios distintos y con nombres de clase diferentes.

A continuación les colocó un link al servicio mediafire (servicio muy bueno por cierto) donde podrán descargar las 2 clases modificadas que les comenté.

Una desventaja que tiene este cambio es que si se actualiza Symfony es necesario volver a reemplazar las clases en cuestión. Hace aproximadamente 10 meses coloqué un ticket en el trac de Propel/Creole solicitando la posibilidad de un cambio o mejora con el código en cuestión, pero hasta la fecha no he tenido una respuesta satisfactoria. Existen otras posibilidades de ajustar el código de Propel/Creole para que trabaje con Schemas de Postgres, pero hasta la fecha, personalmente considero que está es una de las más sencillas.

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.