marzo
28

Minimizar consultas SQL repetidas en CakePHP

Posted In: CakePHP by Mauro Zadunaisky

Trabajando en optimización de performance de una aplicación CakePHP noté que una consulta SQL se estaba ejecutando más de una vez, siempre igual y devolviendo el mismo resultado.

El método del modelo que generaba la consulta era más o menos así

/**
 * Determina si un producto está habilitado
 *
 * @param int $id Id del producto en base de datos
 * @return boolean
 * @access public
 */
public function isEnabled($id) {
	$product = $this->find('first', array(
		'conditions' => array('Product.id' => $id),
		'fields' => array('enabled')
	));
	if (empty($product)) {
		$out = false;
	} else {
		$out = !empty($product['Product']['enabled']);
	}
	return $out;
}

En algunos casos, al momento de llamar a isEnabled ya teníamos disponible el array con los datos del producto porque habíamos ejecutado un find previamente, así que el método se podría reescribir pasándole el array con todos los datos en vez del id, y con eso ya nos ahorraríamos consultas SQL repetidas

/**
 * Determina si un producto está habilitado
 *
 * @param array $product Array del producto retornado por un find()
 * @return boolean
 * @access public
 */
public function isEnabled($product) {
	return !empty($product['Product']['enabled']);
}

Sin embargo, esto hizo fallar algunos casos de testing porque en otros lugares de la aplicación el método necesitaba recibir el id el producto y todavía no se había leído el producto desde base de datos como para pasar el array.

La refactorización final quedó así:

/**
 * Determina si un producto está habilitado
 *
 * @param mixed $product Array del producto retornado por un find() o id del producto
 * @return boolean
 * @access public
 */
public function isEnabled($product) {
	if (!is_array($product) or empty($product['Product']['enabled'])) {
		$product = $this->find('first', array(
			'conditions' => array('Product.id' => $product),
			'fields' => array('enabled')
		));
	}
	if (empty($product)) {
		$out = false;
	} else {
		$out = !empty($product['Product']['enabled']);
	}
	return $out;
}
noviembre
26

Tomemos como ejemplo un modelo Page que cumple con las siguientes condiciones:

  • Tiene asociado el TreeBehavior, es decir, cada Page del sistema podría ser hija de otra Page.
  • Tiene un campo created en donde se guarda la fecha en que la página fue creada.
  • Al momento de mostrar el árbol de páginas quiero que se ordenen por fecha de creación y, si hay dos páginas con la misma fecha de creación, que se ordenen por número de id.

Esto podría conseguirse mediante el siguiente código

$pages = $this->Page->find(
   'threaded',
   array('order' => array('Page.created ASC, Page.id ASC'))
);

Lo cual genera la siguiente consulta

SELECT `Page`.`id`, `Page`.`parent_id`, `Page`.`lft`, `Page`.`rght`, `Page`.`title`, `Page`.`text`, `Page`.`created` FROM `pages` AS `Page` WHERE 1 = 1 ORDER BY `Page`.`created` ASC, `Page`.`id` ASC

Hasta acá todo perfecto, pero si necesitamos generar un listado en forma de árbol para poder usar en un select generado por el Form helper, escribiríamos lo siguiente

$this->Page->generateTreeList();

Que genera la siguiente consulta

SELECT `Page`.`id`, `Page`.`title`, `Page`.`lft`, `Page`.`rght` FROM `pages` AS `Page` WHERE 1 = 1 ORDER BY `Page`.`lft` asc

Como podemos ver el problema es que generateTreeList siempre ordena a través del campo lft y no hay posibilidad de definir un ordenamiento diferente (puedes ver el código aquí)

Intento de solución

Ya que no podemos reordenar los datos al momento de hacer la consulta, intentaremos reordenarlos cuando guardamos las páginas. Para eso, el treeBehavior tiene un método llamado reorder al que podemos pasarle por parámetros cómo queremos que se reordene nuestro árbol. Cada vez que llamamos a reorder, los campos lft y rght del modelo se actualizan automáticamente, de modo que la próxima vez que hagamos un generateTreeList recibiremos los datos ordenados de la forma que queríamos.

El problema con esta solución es que reorder sólo nos permite reordenar a través de un único campo, con lo cual no nos sirve para nuestro ejemplo. Hemos abierto un ticket sugiriendo una pequeña modificación para que nos permita reordenar a través de consultas más complejas, puedes verlo aquí: http://cakephp.lighthouseapp.com/projects/42648/tickets/1263-complex-sort-on-tree-reorder

Solución final

Podemos crear un behavior propio que extienda al treeBehavior oficial y que sobreescriba el método reorder, con las modificaciones que se sugieren en el ticket. De este modo no necesitamos modificar el núcleo de CakePHP.

mayo
2

avz-database

Para un proyecto que estamos desarrollando necesitamos una base de datos con todas las ciudades de Argentina. Buscando por la web encontramos el blog de Kerzek quien amablemente comparte una base de datos en SQL Server con todas las localidades, departamentos y provincias.

Como nosotros trabajamos con MySQL, tuvimos que realizarle algunos ajustes al script para que funcione correctamente. También aprovechamos para hacerla compatible con las convenciones del framework CakePHP. Puedes descargar el script a continuación (el archivo está en UTF-8):

Script MySQL de localidades, departamentos y provincias de Argentina.
Read More