Caché en el desarrollo de drupal 6

Probablemente la dificultad más grande en el mundo del desarrollo es la velocidad de respuesta, un sistema de caché optimizado y un desarrollo orientado a utilizarlo nos supone una ventaja en rendimiento que no podemos desaprovechar.

En este artículo hablaré un poco de la caché básica para el desarrollo de módulos a medida en Drupal, pero las cosas mejor desde el principio.

¿Qué es la caché?

El mecanismo de cache mejora el rendimiento de forma transparente al usuario guardando datos en previsión a que sean solicitados más adelante y de esta forma servirlos más rápidamente. Cuando se hace una petición por el dato en concret, primero se busca en la caché, y si no está se devuelve normalmente realizando una consulta a base de datos y recuperando la información "en vivo". Esto proporciona una ventaja en velocidad, puesto que el dato es muchísimo más rápido de recuperar desde la caché que desde su medio original, el inconveniente es la frescura de datos, si esta información es modificada en la base de datos, puede pasar un tiempo hasta que la caché la muestre actualizada.

Como ejemplo ilustrativo, si vamos a recoger manzanas y alguien las ha puesto ya en un cesto, será mucho más rápido cogerlas de ahí que del propio árbol. (foto de mcmrbt)

Introduciendo caché en nuestros módulos a medida

El proyecto de ejemplo parte de un tipo de contenido creado con CCK llamado "libro", que tiene dos campos, uno llamado description que almacena la descripción de los libros y otro llamado pagenumber que almacena su número de páginas.

Antes de empezar deberemos crear un módulo, con dos ficheros básicos, el .info que contiene la metainformación del módulo y el .module que contiene el código, el módulo del ejemplo se llama libro_cache, entonces el libro_cache.info podría ser más o menos así:

;$Id$ 
name = Libro Cache
description = Ejemplo de Cache para el módulo libro
package = Demo Package
core = 6.x 

El primer paso para desarrollar el libro_cache.module es crear una página donde listar todos los libros, para ello necesitamos implementar un hook_menu para definir la url en la que estará dicho listado, por ejemplo:

/**
 * Implementation of hook_menu().
 */
function libro_cache_menu() {
  $items = array();
  $items['show-libros'] = array(
    'title' => t('Show libros page'),
    'page callback' => 'show_libros',
    'access callback' => TRUE,
    'type' => MENU_CALLBACK,    
  );
  return $items;
}

De esta forma definimos una página que no tiene entrada en el menú (MENU_CALLBACK) y que responderá a la url show-libros.

La función definida en el 'page callback' del hook_menu la he llamado show_libros y es la que debe realizar las operaciones para cargar los libros desde base de datos o caché y devolver el resultado en html.

/**
 * Implementation of hook_theme().
 */
function libro_cache_theme() {
  return array(
    'show_libros' => array('arguments' => array('libros')),
  );
}
function show_libros() {
  $libros = get_libros();
  return theme('show_libros', $libros);
}

Necesitamos implementar un hook_theme, para poder realizar el output de nuestro módulo a través de una función de theme que puede ser llamada o bien en el mismo módulo o a través de cualquier plantilla. Quedaría más o menos así:

function theme_show_libros($libros) {
  $output = '';
  $output .= '<div class="libros">';
  foreach ($libros as $libro) {
    $output .= '<h3>'.$libro['title'].'</h3>';
    $output .= '<p>'.$libro['description'].'</p>';
    $output .= '<p>'.$libro['pagenumber'].'</p>';
  }
  $output .= '</div>';
  return $output;
}

Podéis encontrar más información sobre esta técnica en la documentación del hook_theme.

En la función show_libros, hemos realizado una llamada a get_libros que es la función que se encargará de recuperar los libros, ya sea desde caché, o desde base de datos directamente. Es aquí donde implementamos las funciones de caché de Drupal. Vamos a verlo con el ejemplo:

function get_libros($reset = false) {
  static $mydata;
  if (!isset($mydata) || $reset) {
    $cache = cache_get('show_libros');
    if (!$reset && !empty($cache->data) && $cache->expire > time()) {
      drupal_set_message('Datos devueltos de la caché de base de datos');
      $mydata = $cache->data;
    }
    else {
      $mydata = get_libros_from_db();
      cache_set('show_libros', $mydata, 'cache', time() + 900);
      drupal_set_message('Datos devueltos consultando a la base de datos sin usar caché');
    }
  }
  else {
    drupal_set_message('Datos devueltos de la caché estática');
  }
  return $mydata;  
}

He añadido llamadas a la función drupal_set message para que se muestre un mensaje que informa desde donde se ha devuelto el dato.

La caché empieza a funcionar desde la primera línea de esta función, la llamada a static $mydata; implica que si este dato en concreto se ha recuperado en alguna otra parte de la página actual, no necesitamos calcularlo de nuevo, lo devolvemos desde la caché estática de php, y por eso se comprueba en la condición if (!isset($mydata) || $reset). Si el dato ya está en la caché estática de php, simplemente se devuelve de allí y nuestra función no hace nada más.

Si es la primera vez que recuperamos el dato en la página actual, entonces comprobamos si lo tenemos guardado en la caché de Drupal utilizando la función cache_get. A nuestro dato le hemos dado el nombre genérico dentro de la caché show_libros, pero si quisieramos cachear un elemento en particular, podríamos darle un nombre de caché relacionado con su identificador. Aquí entra un nuevo elemento en juego, que es la fecha de caducidad, el expire. Puede pasar que el dato esté en la caché, pero esté ya caducado, entonces se comporta como si no hubiera estado nunca, pero si cache_get encuentra el dato buscado y no está caducado, lo cargamos en nuestra variable con la sentencia $mydata = $cache->data; y lo devolvemos, no hemos hecho la consulta inicial, sino que hemos devuelto el dato desde la caché.

En el caso de que el dato no esté en la caché estática de php y no haya sido encontrado a través de cache_get por Drupal, o sí haya sido encontrado pero esté ya caducado, no queda más remedio que recuperarlo desde base de datos, en este caso con la función get_libros_from_db pero es muy importante que ya que lo estamos recuperando "fresco" desde la base de datos, aprovechemos para almacenarlo en caché y que las próximas llamadas sí que lo recuperen de caché.

Para eso utilizamos cache_set, pasándole como parámetros el nombre de identificador de caché que luego vamos a utlizar para cache_get, el dato que queremos almacenar y la tabla de caché en la que queremos guardarlo, 'cache' para almacenarlo en la genérica, además le pasamos como parámetro cuánto tiene que durar en caché (expire), en el ejemplo le he pasado time()+900, lo que quiere decir que durará 900 segundos a partir del momento en el que se guarda.

El parámetro $reset que recibe y comprueba esta función es imprescindible para poder realizar llamadas que eviten la caché, por ejemplo desde el interfaz de administración, o llamadas de prueba para comprobar el proceso, si llamamos a get_libros() tomará $reset como falso por defecto y usará el sistema de caché, pero si lo llamamos con get_libros(TRUE); ignorará el sistema de caché y lo devolverá desde base de datos.

Para completar el módulo de pruebas, necesitamos un par de funciones extra, get_libros_from_db recupera la información desde base de datos y podría ser más o menos así:

function get_libros_from_db() {
  $libros = array();
  $sql = "SELECT n.nid, n.title, l.description, l.pagenumber FROM {node} n JOIN {libro} l ON n.vid = l.vid";
  $result = db_query($sql);
  while ($data = db_fetch_object($result)) {
    $libros[$data->nid]['title'] = $data->title;
    $libros[$data->nid]['description'] = $data->description;
    $libros[$data->nid]['pagenumber'] = $data->pagenumber;
  }
  return $libros;
}

Y también tenemos que tener en cuenta que si trabajamos con contenido en Drupal, todo son nodos, por lo que cuando se borra un nodo, tendríamos que hacer algo con la caché para que no se recupere contenido que no existe, para ello utilizamos la función cache_clear_all.

function libro_cache_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
  if (($op == 'insert' || $op == 'update' || $op == 'delete' || $op == 'delete revision') && $node->type == 'libro') {
    cache_clear_all('show_libros');
  }
}

El hook_nodeapi interviene cuando se guarda un nodo, ya sea para crearlo, modificarlo, borrarlo... Si se inserta, borra o actualiza un nuevo libro, limpiamos el dato de los libros de la caché para evitar en lo posible mostrar contenido caducado.

¿Y si quiero usar mi propia tabla de caché?

El ejemplo que hemos visto arriba utiliza la tabla genérica de caché de Drupal, pero es posible que vayamos a guardar mucha información en caché o que la queramos tener separa de la información genérica de Drupal. Podemos generar nuestra propia tabla de caché y utilizarla en el módulo o módulos a medida que construyamos.

Para esto vamos a necesitar tres funciones y un nuevo fichero, a través de los ficheros .install el motor de Drupal sabe qué operaciones debemos realizar cuando se instala y desinstala un módulo. El fichero libro_cahe.install quedaría más o menos así:

/**
 * Implementation of hook_install().
 */
function libro_cache_install() {
  drupal_install_schema('libro_cache');
}

/**
 * Implementation of hook_uninstall().
 */
function libro_cache_uninstall() {
  drupal_uninstall_schema('libro_cache');
}

/**
 * Implementation of hook_schema().
 */
function libro_cache_schema() {
  $schema = array();
  $schema['cache_libro'] = drupal_get_schema_unprocessed('system', 'cache');
  return $schema;
}

El hook_install se ejecuta cuando se instala el módulo y el hook_uninstall cuando se desinstala, ambos hacen uso del schema api y referencian al hook_schema, que es quien tiene la información de la tabla que vamos a utilizar, en este caso replicamos la tabla de caché nativa de drupal, que para la mayoría de gestiones de caché será suficiente. A nuestra tabla de caché, la hemos llamado cache_libro.

Para hacer uso de nuestra nueva tabla, habremos de modificar las llamadas a las funciones de caché, cache_get, cache_set y cache_clear_all.

$cache = cache_get('show_libros', 'cache_libro');
cache_set('show_libros', $mydata, 'cache_libro', time() + 900);
cache_clear_all('show_libros', 'cache_libro');

Resumen de funciones de caché en Drupal

  • cache_get - Se utiliza para recuperar datos desde la caché, se le pasa por parámetro el nombre de dato buscado, y opcionalmente la tabla en la que se debe buscar y devuelve un objeto con los datos cacheados, y cuándo expiran.
  • cache_set - Se utiliza para almacenar datos en la caché, se debe enviar el nombre del dato que identificará al mismo, el dato en sí, el nombre de la tabla en la que se quiere almacenar (usar 'cache' para la genérica') y cuándo debe caducar el dato, se puede utilizar time() + un tiempo en segundos.
  • cache_clear_all - Sirve para limpiar un dato de la caché, se le debe pasar por parámetro el nombre del dato buscado y opcionalmente la tabla en donde está almacenado.

Como habéis podido observar, no es nada difícil implementar la caché en módulos a medida en Drupal, lo que sí es realmente importante y complejo es diseñar la estrategia de cacheo, cuándo se debe cargar de caché, cuando se debe limpiar... e identificar los puntos clave para mejorar el rendimiento del sistema.

Adjunto el módulo para que le podáis echar un vistazo a todo el ejemplo junto.

AdjuntoTamaño
libro_cache.zip3.19 KB

5 comentarios, participa en la conversación

  • Realmente interesante.
    Me ha gustado mucho la similitud de las manzanas.

  • Si señor, muy completo el articulo, muchas gracias caballero!

  • Me alegra mucho que os haya gustado! gracias por pasaros a comentar :)

  • Hola, muy buena la aportación pero a mi me gustaría saber como no introducir un objeto en cache. Te pongo en situación en mi drupal existen dos roles: cliente y empresa. Si entras en uno de los dos a través de hook_link_logo he conseguido que el logo apunte a sitios diferentes pero drupal lo guarda en su cache. Mi pregunta es como podría evitar que drupal cacheara la url del logo. Un saludo y gracias

  • No es un hook es una function creada por un compañero, XD. Y controla los roles para realizar los cambios. Funcionar funciona pero cuando limpias la cache. Y si todas las caches de drupal las tenemos en off. Un saludo

  • Comentar

    CAPTCHA
    Esta pregunta sirve para distinguir si eres un humano o un spambot.
    5 + 7 =
    Resuelve esta operación e introduce el resultado, por ejemplo, para 1+3, introduce 4