Modelos experimentales de persistencia para aplicaciones web (II): Object Freezer-Relational

En el anterior post vimos un invento de Sebastian Bergmann llamado Object Freezer, que consiste en almacenar objetos PHP en CouchDB de forma automática. Es un buen método para no tener que definir esquemas de bases de datos, escribir sentencias SQL, hacer mapeo objeto-relacional… en definitiva, es una solución que nos puede ahorrar un montón de trabajo.

Pero, una vez aceptadas las ventajas de la congelación, quizá sea necesario guardar esos objetos en una base de datos diferente a CouchDB. Por ejemplo, en MySQL. Y de esto trata el presente artículo: de congelar objetos y guardarlos en MySQL.

Un brevísimo repaso a la congelación

Congelar un objeto (“freeze”) es convertirlo en un array manteniendo los objetos hijos y evitando duplicidad de los mismos. La operación inversa, la descongelación (“thaw”), sería recoger ese array y convertirlo de nuevo en el objeto original.

¿Por qué MySQL, si con NoSQL nos ahorramos todo el trabajo?

  1. A veces no es viable o rentable cambiar de servidor de bases de datos, por la inversión económica y de formación que requieren los sistemas tradicionales.
  2. Interoperabilidad: si en el futuro queremos migrar la aplicación, quizá poder manipular la información con SQL facilite la tarea.
  3. Rendimiento: las bases de datos relacionales se basan en algoritmos muy evolucionados y rápidos, y sistemas como MySQL-MyISAM han demostrado un rendimiento muy alto.

Entonces… ¿Se trata de guardar arrays en MySQL?

Exacto. Eso es lo que hace Object Freezer-Relational, una extensión de Object Freezer escrita por un servidor :-)

Lo primero que debemos pensar a la hora de guardar objetos en MySQL es: ¿cuál es el esquema de tablas? Porque si con CouchDB éramos NoSQL y guardábamos la información en arrays JSON, ahora tenemos que ceñirnos al álgebra relacional, las tablas, los índices y las claves primarias.

Así que este es el esquema que podemos usar:

  • Tabla objects
    • id: el ID único del objeto.
    • className: indica la clase de la cual es instancia el objeto almacenado.
    • isDirty y isRoot: dos atributos que utiliza Object Freezer internamente.
  • Tabla properties
    • name: nombre del atributo.
    • value: contiene el valor del atributo o bien un ID si el atributo hace referencia a otro objeto.
    • type: tipo de dato (int, float, string, boolean, array, objeto…).
    • object_id: ID del objeto al que pertenece el atributo. Hace referencia a objects.id.

Llegados a este punto uno podría pensar: “hey, te estás cargando el modelo relacional”. Efectivamente. Si queremos guardar objetos sin definir su esquema de BD, no nos queda más remedio que soluciones experimentales como la que te presento hoy. Sí, es una “des-normalización”, pero gracias a ella te ahorras escribir SQL.

Object Freezer-Relational se encarga de gestionar este esquema. De hecho, no es siquiera necesario crearlo, ya que lo hace automáticamente.

Vale, ya veo que mola. ¿cómo se usa?

Usar Object Freezer-Relational es tan sencillo como usar Object Freezer indicando los datos de acceso a MySQL:

$storage = new Object_Freezer_RelationalStorage(
    new Object_Freezer,
    NULL,
    FALSE,
    new MysqlStorage(
        "localhost",                    //Servidor MySQL
        "freezer",                      //Usuario
        "passw0rd",                     //Contraseña
        "freezer",                      //Base de datos
        3306,                           //Puerto
        MysqlStorage::ENGINE_INNODB));  //Motor MySQL

A partir de ahí, podemos usar el objeto $storage de la misma forma que el Object Freezer original, con CouchDB, ya que es la misma API.

Object Freezer-Relational está disponible en SourceForge. Puedes descargarlo y probarlo, echar un vistazo al código o leer la documentación si te interesa. Y por supuesto, comentar qué te parece la idea de guardar objetos en una base de datos MySQL :-)

Modelos experimentales de persistencia para aplicaciones web (I): congelación de objetos con Object Freezer

Hoy comienzo una serie de artículos dedicados a exponer diferentes patrones y arquitecturas de software para un problema tradicional: el almacenamiento de datos. Hacer consultas SQL a una base de datos relacional es una de las soluciones más extendidas, pero no es la única. Hay alternativas como las que te voy a presentar en este artículo y los siguientes.

Disclaimer

Las ideas que voy a presentar son experimentales. Son conceptos, con su discusón y su implementación (casi siempre parcial), pero no son estándares ampliamente probados y usados en el mundo real. El objetivo de estos artículos es reflexionar, discutir, probar y ver que algunas soluciones pueden ser útiles en ciertos escenarios, y otras quizá no valgan para nada. :-P

La congelación de objetos

La primera idea que voy a presentar es un concepto de Sebastian Bergmann. Los datos que se almacenan deben ser objetos de PHP. Se trata de “congelar” los objetos de una forma similar a como lo hacen las funciones de serialización. La diferencia es que el método de congelación entiende las relaciones entre objetos (es decir, que un atributo de un objeto sea, a su vez, otro objeto), de tal forma que si el objeto A tiene una referencia a B y el objeto C tiene una referencia a B también, al congelar los objetos A y C la referencia a B será un puntero al objeto congelado B, en lugar de congelar B dentro de A y B dentro de C, duplicando los datos, como hace serialize().

Pero ¿qué quiere decir congelar objetos? Pues sencillamente convertirlos en arrays para poder guardarlos en CouchDB, una base de datos orientada a documentos en la que los datos se almacenan en JSON.

Para comprender el funcionamiento de Object Freezer, nada mejor que un artículo de su autor: Freezing and Thawing PHP Objects.

¿Qué problemas soluciona Object Freezer?

  1. La definición y uso de esquemas relacionales. Ya no son necesarias las tablas, claves primarias, foreign keys, etc.
  2. El mapeo objeto-relacional. No hay que definir la conversión de tablas a objetos y viceversa.
  3. El almacenamiento sencillo de atributos vectoriales: es una forma fácil y automática de guardar arrays en la base de datos.
  4. ¡Tipado dinámico! En PHP se pueden añadir nuevos atributos a un objeto ya existente en tiempo de ejecución. No sería posible guardar este tipo de objetos en esquemas relacionales fijos, pero con Object Freezer es posible.

Además, tiene la ventaja de que, al guardar los objetos en CouchDB se maneja el versionado de los objetos, ya que CouchDB controla los cambios de todos sus registros.

Dame algo de código

//Una clase cualquiera
class Coche {
    private $color = 'rojo';
} 

//Una instancia de una clase cualquiera
$ferrari = new Coche();

require_once 'Object/Freezer/Storage/CouchDB.php'; 

//Instancia el freezer indicando los datos de conexión a CouchDB
$storage = new Object_Freezer_Storage_CouchDB('test', new Object_Freezer, TRUE, 'localhost', 5984); 

//Congela el objeto y lo almacena en CouchDB, devolviendo el ID auto-generado
$id = $storage->store($ferrari);

Así de sencillo. CouchDB asigna un ID único a cada nuevo registro. Para leer un registro:

//Devuelve el objeto de tipo Coche con $id
$objeto = $storage->fetch($id);

¿Es adecuado Object Freezer para mi proyecto?

La decisión de almacenar datos sin un esquema fijo es tan importante como la de usar una base de datos relacional o una NoSQL. ¿Es NoSQL bueno? No vamos a discutirlo aquí, pero sí citaré un tweet que leí una vez:

Usar NoSQL es como ir sin calzoncillos. Es cómodo, pero peligroso.

No obstante, hay una cuestión a tener en cuenta en la decisión: si usamos Object Freezer es cierto que la base de datos será schema-less, pero no es cierto que nuestros datos no estén estructurados, ya que ¡usamos objetos! Sería más correcto decir que los datos no están estructurados en la base de datos, pero sí en el código.

Si hemos hecho un buen diseño de las clases y sus relaciones, Object Freezer puede ser una solución rápida y fácil para guardar esos objetos en una base de datos muy interesante como CouchDB.

Sin embargo, no creo que sea buena idea para proyectos…

  • con fuerte necesidad de integridad referencial, o
  • que no estructuran toda la información con objetos, o
  • grandes, o
  • si crees que el modelo almacenamiento de datos cambiará, o
  • si no sabes usar las vistas de CouchDB para obtener listas de datos filtradas, indizadas, etc.

Creo que con esto es suficiente introducción. Puedes consultar la presentación que he citado arriba y el blog de Sebastian Bergmann para tener más información sobre Object Freezer, que por cierto es software libre :-)

Esto es todo por hoy. Ahora te toca a ti: ¿qué te parece el concepto de “congelar los objetos”? ¿Usarías Object Freezer? ¿Usarías CouchDB? ¿Conoces soluciones similares para otros lenguajes de programación?

El patrón Strategy y las llamadas dinámicas en PHP

Sigo explorando las novedades de PHP 5.3 y, entre ellas están las llamadas dinámicas a métodos estáticos, según he visto en esta interesante presentación de Ilia Alshanetsky. Veamos un ejemplo:

class Aplicacion {

	public static function getVersion() {
		return "1.2";
	}

}

$metodo = "getVersion";

Aplicacion::$metodo(); //Devuelve "1.2"

Se ve claramente que podemos llamar a un método estático cuyo nombre no definimos al escribir el código, sino que está en una variable. Esta característica ya estaba disponible en versiones anteriores de la rama PHP 5, tanto para clases como para métodos, pero no estáticos:

class Conexion {

	public function conectar($servidor, $usuario, $clave) {
		echo "Mira Mamá, me estoy conectando!";
	}

}

$clase = "Conexion";
$método = "conectar";

$objeto = new $clase();
$objeto->$método("localhost", "pepito", "grill0");
//Imprimirá "Mira Mamá, me estoy conectando!"

Bueno, y esta característica parece interesante, pero ¿cómo podemos aplicarla a la vida real? Uno de los ejemplos más claros que se me ocurre es la implementación del patrón de diseño Strategy. En este sencillo patrón se trata de cargar una u otra implementación de una clase dependiendo de alguna variable o acción del usuario. Por ejemplo, imaginemos que queremos conectarnos a bases de datos Oracle o MySQL, según qué servicio escoja el usuario desde un formulario. Primero definimos una clase abstracta Conexion que declare los métodos que las implementaciones han de desarrollar, y escribimos sendas clases hijas para Oracle y MySQL:

abstract class Conexion {

	private $recurso;

	public function conectar($usuario, $clave) { }
}


class ConexionOracle {

	public function conectar($usuario, $clave) {
		$this->recurso = oci_connect($usuario, $clave);
	}

}

class ConexionMysql {

	public function conectar($usuario, $clave) {
		$this->recurso = mysql_connect("localhost", $usuario, $clave);
	}

}

/*
 * $_POST['dbms'] proviene de un cuadro desplegable (option select) en que se
 * da a escoger entre oracle y mysql.
 * ucfirst() pone en mayúscula la primera letra
 */

$conector = "Conexion" . ucfirst($_POST['dbms']);
$instancia = new $conector;

try {
	$instancia->conectar($_POST['usuario'], $_POST['clave']);
} catch (Exception $e) {
	echo "Lo siento, pero por algún motivo la conexión ha fallado";
}

En fin, esto es todo, creo que es evidente la potencia de esta característica del lenguaje, que viene a intentar evitar el uso de eval() y dar al lenguaje un poco más de elegancia, al estilo del class.forName() de Java. Por cierto, para una descripción más profunda del patrón Strategy te recomiendo este interesante artículo de Jack Herrington sobre patrones implementados en PHP.

Patrones de desarrollo web: componentes HTML reutilizables

Cuando construimos aplicaciones con muchos formularios u otras secciones con fragmentos repetitivos, a veces utilizamos componentes, como puede ser un cuadro desplegable (combo box u option select, llámale como quieras) para elegir una ciudad. Ya que las páginas se generan en el servidor (PHP, Java, Ruby…), se han inventado diversos artificios para reutilizar estos componentes: simples funciones o variables que devuelven el código HTML deseado, mini-plantillas (Smarty), elements de CakePHP, etc.

Pero ¿y si no estamos generando esos formularios en el servidor, sino dinámicamente en JavaScript? La solución en el cliente es disponer de componentes de diseño como fragmentos de HTML invocables por AJAX. Así de sencillo.

Por ejemplo, imaginemos que nuestra aplicación/web tiene muchos formularios en los que elegir una ciudad en un control option select. Si estamos generando formularios a través de JavaScript, sería muy útil disponer de una URL invocable a través de AJAX que nos devolviese algo como

<select>
	<option value="1">Vigo</option>
	<option value="2">Murcia</option>
	<option value="3">Sevilla</option>
</select>

Y si agregamos algunas opciones…

<?php $opciones = array("Vigo", "Murcia", "Sevilla") ?>
<select name="<?php echo $_GET['nombre'] ?>">
<?php foreach ($opciones as $n=>$nombre) { ?>
	<option value="<?php echo $n ?>" <?php if ($_GET['valor'] == $n) echo "selected=""" ?>><?php echo $nombre ?></option>
<?php } ?>
</select>

Entonces tenemos algo como form_ciudades.php?nombre=xxx&valor=yyy que nos devolverá un option select de nombre xxx con el valor yyy seleccionado por defecto.

Si combinamos esto con alguna función que envuelva peticiones AJAX y nos devuelva el contenido requerido de forma sencilla, conseguiremos un código fácilmente legible. Aquí va un ejemplo real, que estoy utilizando para una práctica de la universidad:

$("#detalles > table").html(
	   "<tr><td>Aerolínea:</td><td>" 
	   + ajax("sn_aerolineas.php?nombre=aerolinea&valor=1") 
	   + "</td></tr>"
	   + "<tr><td>Origen:</td><td>" 
	   + ajax("sn_localidades.php?nombre=origen&valor=1") 
	   + "</td></tr>"
	   + "<tr><td>Destino:</td><td>" 
	   + ajax("sn_localidades.php?nombre=destino&valor=1") 
	   + "</td></tr>");

Para quien no conozca jQuery, la función $(“#detalles > table”) devuelve la tabla que hay dentro del elemento del contenedor “detalles”, y la función html() cambiará su innerHTML. ajax es una pequeña y sucia función que me devuelve directamente una respuesta AJAX.

Este patrón se puede aplicar para muchos más casos, como selectores de fecha (input + calendario), o incluso llamadas que devuelvan datos mucho más pequeños, como la foto de un usuario o una lista de categorías. Pero recuerda que este patrón debe ser aplicado sólamente cuando se generan elementos dinámicamente. Es decir, usa AJAX sólo cuando sea necesario para mejorar la usabilidad y/o el rendimiento de tu sitio.

Patrones de desarrollo web: Multivista (II)

Aquí tienes la procrastinada segunda parte del patrón Multivista. En este post veremos cómo añadir lógica a las vistas y cuáles son los límites de este patrón. Como caso práctico del Multivista orientado a objetos tendremos este mismo blog.

¿Cuándo es necesario este segundo método?

  • Cuando hay plantillas sin salida de texto: PDF, imágenes, flash…
  • Cuando se utilizan helpers o librerías para generar documentos: Pear-Html, DOMDocument, etc
  • Cuando no se quiere emplear un sistema de plantillas.

El patrón Multivista orientado a objetos es similar al patrón Template Two Step View, y se introduce normalmente en una estructura Modelo-Vista-Control de tres capas, como ya expliqué en la primera entrega. Se basa principalmente en la abstracción de las acciones (“postergar los problemas”, como dice mi compañero Dors). Si en el primer método cargábamos los mismos datos introduciéndolos en una u otra plantilla, ahora tendremos una abstracción de tres niveles, donde podremos cargar unos datos comunes (ahí suelen estar las llamadas a los DAO) en el primero, ejecutar unas acciones específicas en el segundo, y el tercer nivel (opcional) serían las plantillas. Veámoslo con un ejemplo:

class VerPost extends Vista {
    public function cargarDatos() {
        Dao::getPost($_GET['id']);
    }

    public post() {
        //Aquí se opera con los parámetros post, si los hubiese (por ejemplo, registrar un comentario)
    }
}

class VerPostHtml extends VerPost {
    //Esta vista podría haber sido implementada con el 1º método
    public function run() {
        $smarty->assign("post", $this->cargarDatos());
        $smarty->display("verPost.tpl");
    }
}

class VerPostPdf extends VerPost {
    //Esta vista requiere código PHP específico, por lo que el 1º método no valdría
    public function run() {
        header("Content-type: application/pdf");
        $datos = $this->cargarDatos());
        $pdf = new Pdf();
        $pdf->addText($datos['titulo']);
        echo $pdf->flush();
    }
}

No es complicado entender este método: la parte de la vista se segrega en dos niveles, el primero es una clase común a todas las vistas del módulo y el segundo nivel son las vistas específicas. Pero el Multivista no se queda en este sencillo código. Podemos establecer clases horizontales comunes a todas las vistas específicas de un tipo completo, por ejemplo, las vistas en PDF:

class ComunPdf {
    static public function run() {
        //Construye una cabecera común a todas las vistas
        header("Content-type: application/pdf");
        $pdf = new Pdf();
        $pdf->addText("Blog de Israel Viana");
        $pdf->addText("www.israelviana.es");
    }
}
class ComunRss {
    static public function run() {
        header("Content-Type: application/xml+rss");
    }
}

Clases comunes a vistas del mismo tipo

Quizás queramos tener alguna funcionalidad común para todas las vistas RSS, por ejemplo. Esta funcionalidad se puede implementar de varias maneras: por una parte las propias clases específicas pueden llamar a las transversales:

class VerPostPdf extends VerPost {
    public function run() {
        $datos = $this->cargarDatos());
        VistaPdf::cabecera();
        $pdf->addText($datos['titulo']);
        echo $pdf->flush();
        VistaPdf::pie();
    }
}

O bien puede hacerse desde el motor de vistas:

//Instancia la clase que corresponda
$vista = $_GET['vista'];
$plantilla = $_GET['plantilla'];
eval("$v = new $vista$plantilla;");

//Invoca los métodos comunes a la plantilla
eval("Comun$plantilla::run();");

//Ejecuta la plantilla
$v->run();

URL amigables

Siendo ortodoxos, esto no estaría dentro del patrón Multivista en sí, pero puede ser una buena forma de manejar las aplicaciones que funcionen con este patrón: veamos cómo interpretar URL del tipo: http://servidor.net/Accion/Vista/param1/param2/…/paramn/vista. Manejar direcciones con esta estructura tiene varias ventajas:

  • Organizar nuestros módulos de una manera bastante intuitiva, separando los scripts “importantes” para nuestra aplicación y las librerías
  • Poder disponer de un script único de acceso (el dispatcher de URL), que maneje los argumentos, los errores y los ataques de inyección SQL y por el estilo. Es decir, se solicite la URL que se solicite, siempre se ejecutará el dispatcher.
  • Cumplir la recomendación de accesibilidad de Jakob Nielsen de hacer URL “hackeables”, para que un usuario pueda cambiar de plantilla sin necesidad de enlaces.
  • Mejorar el posicionamiento en buscadores de nuestro sitio web (SEO).

Lo primero es redirigir todas las peticiones al dispatcher. En Apache se hace así:

RewriteEngine on RewriteRule .* index.php

Finalmente, el código del dispatcher tendría esta pinta (el código está bastante simplificado, la versión real la publicaré con el resto del código del blog):

//Vistas
$vistas = array(
    'Index'     => array('nombre'=>"Index",  'plantillas'=>array("Html", "Rss", "Rdf")),
    'Post'      => array('nombre'=>"Post",       'plantillas'=>array("Html", "Rss", "Rdf")),
    'Archivo'   => array('nombre'=>"Archivo",    'plantillas'=>array("Html", "Rss", "Rdf")),
    'Pagina'    => array('nombre'=>"Pagina",     'plantillas'=>array("Html", "Rss", "Rdf")),
    'Proyectos' => array('nombre'=>"Proyectos", 'plantillas'=>array("Html"))
);

$GLOBALS['vista_por_defecto'] = "Index";
$GLOBALS['plantilla_por_defecto'] = "Html";

$peticion = $_SERVER['REQUEST_URI'];
$parametros = split('/', substr($peticion, strlen(RUTA_R))); //Quita la URL base

//Obtiene el módulo
$v = (strlen($parametros[0])) ? ucfirst($parametros[0]) : $GLOBALS['vista_por_defecto'];

//El módulo especificado no existe
if (!array_key_exists($parametros[0], $vistas)) {
    include("404.php");
    die();
}

//Elimina el módulo de los parámetros
array_shift($parametros);

//Protege contra el error de no incluir barra final en URL que no contienen vista específica, por ejemplo /Post/31/titulo-del-post
$ultimo_parametro = ucfirst($parametros[count($parametros)-1]);
if (array_search($ultimo_parametro, array_merge(array(null), $vistas[$v]['plantillas']))) {
    $p = $ultimo_parametro;
    array_pop($parametros);
} else {
    $p = $GLOBALS['plantilla_por_defecto'];
}

//Verifica si está existe la vista
if (array_key_exists($v, $vistas)) {
    $vista = $vistas[$v];
    //Verifica si existe la plantilla
    //No vale negar con ! porque si el índice es 0, la condición no se cumpliría
    if (null == array_search($p, array_merge(array(null), $vista['plantillas']))) die("La plantilla especificada no existe");
} else {
    die("La sección especificada $v no existe");
}

//Pasa los parámetros a la vista
$GLOBALS['parametros'] = $parametros;
$smarty->assign("parametros", $GLOBALS['parametros']);

$GLOBALS['vista_actual'] = $v;
$GLOBALS['plantilla_actual'] = $p;

//Carga las clases vista y plantilla
require_once("vistas/Vista$v.php");
require_once("vistas/$v/$v$p.php");

//Instancia la plantilla
eval("$vista = new $v$p();");
//Métodos comunes (transversal) al tipo de plantilla
if (file_exists("vistas/Plantilla$p.php")) include("vistas/Plantilla$p.php");
//Ejecuta la plantilla
$vista->run();

Y con esto, un DAO y poco más (veremos en sucesivos artículos qué es lo que falta) tendremos un marco de trabajo para aplicaciones web multivista. No se trataría de un MVC en toda regla, pero sí de una arquitectura de tres capas válida para aplicaciones robustas.