A blog about data, information and Tech by Mario Alberich

        

Magento: cómo iterar más agilmente sobre una colección extensa de modelos

Una de las primeras cuestiones que se encuentra un programador en Magento es que es necesario ser estricto en el uso de los recursos (memoria y CPU), ya que en caso contrario es fácil quedarse sin memoria.

Una de las situaciones típicas en este proceso son el manejo de colecciones de productos.  La carga de una página de categoría que contenga una docena de productos no es un problema, pero sí lo es cuando queremos procesar centenares o miles de productos en bloque.  En ese caso, no es extraño acabar viendo un mensaje como éste:

Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 49 bytes) in ... on line ...


La primera reacción, la más natural, es aumentar la memoria. Pero eso no es una solución ni ayuda a entender el funcionamiento de Magento. Además, lo que hoy son 128 MB, mañana es el doble, y así va creciendo.

Uso de Mage::getModel()->getCollection()


La forma clásica en Magento de recopilar un conjunto de modelos es utilizando getCollection(), para luego definir los atributos a recuperar, los filtros de la consulta, joins, el límite y el offset. Más sobre esto en otro artículo.

Finalmente, se realiza la llamada al método load(), que es el método que ejecuta realmente la consulta contra el servidor. En ese momento tenemos todos los modelos (que son instancias de la clase modelo correspondiente) en memoria, con todo lo que ello implica:

Mage::getModel("catalog/product")
->getCollection()
->addAttributeToSelect('sku')
->setPage(1)
->setPageSize(10)
->load();

Aunque esta aproximación es factible para consultas con límites no muy largos, el incremento en el número de productos recuperados dispara el uso de memoria. Lo que sucede es que para cada ítem, Magento genera una instancia del objeto del modelo, y eso implica instanciaciones, relaciones con otras clases,  y al fin y al cabo mucha más memoria de la necesaria para lo que ocupan los datos.

Uso del iterador


Por suerte para los que no siempre necesitamos ese volumen de información, Magento proporciona el Resource Iterator. El archivo implicado se encuentra en app/code/core/Mage/Core/Model/Resource/Iterator.php, y podrás ver que es bastante sencillo. En concreto, el método que nos interesa (walk) contiene el siguiente código:

    public function walk($query, array $callbacks, array $args=array(), $adapter = null)
{
$stmt = $this->_getStatement($query, $adapter);
$args['idx'] = 0;
while ($row = $stmt->fetch()) {
$args['row'] = $row;
foreach ($callbacks as $callback) {
$result = call_user_func($callback, $args);
if (!empty($result)) {
$args = array_merge($args, $result);
}
}
$args['idx']++;
}
return $this;
}

Lo que hace este método es cargar la lista de modelos de la colección, uno por uno, y para cada cual ejecuta la (o las!) $callbacks que se han enviado.  A cada una de las callbacks se le envía la lista de argumentos $args.

Ojo también al detalle en el interior if (!empty($result)) {...}, porque es interesante.  Si enviamos más de una función de callback, la segunda función puede recibir el valor retornado por la primera, y la tercera lo retornado por la segunda, y así hasta el final.  Esto nos permite trabajar en operaciones muy atomizadas (como siempre, sin abusar).

También es interesante comprobar que el contenido recuperado de la base de datos se añade al array $args dentro del índice 'row', por lo que los datos retornados del registro estarán en $args['row'].  También es interesante darse cuenta que los datos recuperados por fetch ya no son un objeto, sino un Array, por lo que todo lo que rodea al objeto modelo queda al margen de esta operativa.

Bueno pues, dicho esto, ¿cómo llamamos al iterador? Con una pequeña diferencia respecto a antes: dejamos de lado load() y utilizaremos getSelect(). Tomando como base la consulta anterior, puedes quitar los límites

$productos = Mage::getModel("catalog/product")
->getCollection()
->addAttributeToSelect('sku');

Mage::getSingleton('core/resource_iterator')->walk($productos->getSelect(), array('funcionCallback'), array('arg1' => 'nombredemitienda', 'arg2' => 'idioma de la tienda', ...));

Bien, entonces ¿ya estamos? No. Es necesario disponer de la función funcionCallback, que es a la que se llamará para cada producto.  Esta función debería tener la forma siguiente y puede tener una forma tan sencilla como:

function funcionCallback($args) {
print_r($args);
}

Com esto puedes ver el contenido de la matriz $args.

Pero por lo comentado arriba, podemos enviar más callbacks, y también podemos enviar callbacks a métodos de este objeto u otros.  Para cada caso, las llamadas serían:

  • Una llamada a un método del propio objeto: array(array($this, 'nombreDelMetodo'))
  • Varias llamadas a métodos del propio objeto: array(array($this, 'metodo1'), array($this, 'metodo2))
  • Combinación de llamadas a métodos y a funciones básicas: array('funcionExterna', array($this, 'metodo1'), array($this, 'metodo2))

Comprobación de las diferencias


Al ejecutar un proceso de recorrido por los modelos de una colección podrás experimentar dos diferencias notables:

  • El volumen de memoria se mantiene estable, que es algo que no siempre sucede cuando se cargan instancias de la clase modelo (más sobre la gestión de memoria en PHP en otro momento).
  • Si no es necesario que instancies un objeto (del producto que estás iterando) dentro de las funciones de callback, vas a notar que la velocidad de proceso se dispara en uno o dos órdenes de magnitud. Esto no siempre es posible, porque a veces es necesario guardar los cambios en el producto, pero si consigues evitarlo, verás que la mejora es impresionante.


 

 

© 2007 and beyond Mario Alberich, licensed under CC-BY-SA unless stated otherwise.