Archivo de 'bases de datos'

Introducción a Doctrine 2

¡Hola! Últimamente sigo liado sumergiéndome en el mundo de ASP.NET, de ADO.NET, y sacando tiempo de debajo de las piedras para un pequeño proyecto en mi querido Zend Framework en PHP. Tras ver ADO.NET Entity Framework sentí la necesidad de usar algo igual en PHP; la solución se llama Doctrine :) La primera pregunta que nos deberíamos hacer es, qué es exactamente Doctrine. Respuesta rápida: Es un ORM de PHP. Pero analicémoslo un poco más.

En primer lugar, ¿qué diantres es un ORM? Viene de las siglas Object-relational mapping, que viene a traducirse como Mapeo objeto-relacional. En concreto es una técnica, o patrón arquitectónico, que permite comunicar dos sistemas distintos, típicamente una base de datos relacional con objetos de un lenguaje orientado a objetos, mediante un sistema que “mapea” (vincula) ambos sistemas. Mucha gente (yo mismo) ha creado a mano sus objetos mapeadores que, junto con objetos usando el patrón Active Record, daban un resultado muy similar a lo que un ORM nos ofrece. Pero oye, un ORM hace todo el trabajo sucio por nosotros ;)

Existen ORMs para la mayoría de lenguajes de programación. Por ejemplo, en Java es muy popular Hibernate, en .NET se utiliza ADO.NET Entity Framework, y en PHP el más usado es Doctrine (hay más). Doctrine está inspirado en Hibernate.

Pero veamos algunas de las características que nos ofrece un ORM como Doctrine 2:

  • Muy sencillo de configurar. De hecho, no es necesario mantener complejos ficheros de configuración XML.
  • Tiene su propio dialecto de SQL, llamado DQL (Doctrine Query Language).
  • Puede generar los modelos (las clases PHP que representan una fila de la BD) a partir de la base de datos. Sólo habría que definir las dependencias entre ellos
  • Del mismo modo, también puede generar la base de datos a partir de los modelos.

¡Fantástico! Él puede crear las tablas, y las consultas por nosotros. Pero veamos ahora lo sencillo que es usarlo:

/* Código de inicialización ... */
$user = new User();
$user->name = "john";
$user->password = "doe";
$em->persist($user);
$em->flush();

Para que este código funcione sólo tenemos que hacer dos cosas:

  1. Creamos el código de inicialización que cree el EntityManager $em. Es el objeto que en última instancia lleva a cabo las acciones.
  2. Definimos las entidades (módulos) especificando en comentarios (o en XML o en YAML) cómo se relaciona con la base de dato

Vamos a ver cómo instalarlo y después haremos un ejemplo sencillo en SQLite.

Instalación

Si bien podemos instalarlo a mano descargándolo desde la página de Doctrine, para esta guía asumiré que se ha instalado con PEAR. Para ello tenemos primer que instalar el canal:

sudo pear channel-discover pear.doctrine-project.org

Y después instalamos el ORM:

sudo pear install pear.doctrine-project.org/DoctrineORM-2.0.1

Ahora lo tenemos instalado. Podemos probar a ejecutar su utilidad de línea de comandos, y nos mostrará esto:

$ doctrine
Doctrine Command Line Interface version 2.0.1
Usage:
[options] command [arguments]
Options:
--help           -h Display this help message.
--quiet          -q Do not output any message.
--verbose        -v Increase verbosity of messages.
--version        -V Display this program version.
--ansi           -a Force ANSI output.
--no-interaction -n Do not ask any interactive question.
Available commands:
help                         Displays help for a command (?)
list                         Lists commands
dbal
:import                      Import SQL file(s) directly to Database.
:run-sql                     Executes arbitrary SQL directly from the command line.
orm
:convert-d1-schema           Converts Doctrine 1.X schema into a Doctrine 2.X schema.
:convert-mapping             Convert mapping information between supported formats.
:ensure-production-settings  Verify that Doctrine is properly configured for a production environment.
:generate-entities           Generate entity classes and method stubs from your mapping information.
:generate-proxies            Generates proxy classes for entity classes.
:generate-repositories       Generate repository classes from your mapping information.
:run-dql                     Executes arbitrary DQL directly from the command line.
:validate-schema             Validate that the mapping files.
orm:clear-cache
:metadata                    Clear all metadata cache of the various cache drivers.
:query                       Clear all query cache of the various cache drivers.
:result                      Clear result cache of the various cache drivers.
orm:schema-tool
:create                      Processes the schema and either create it directly on EntityManager Storage Connection or generate the SQL output.
:drop                        Drop the complete database schema of EntityManager Storage Connection or generate the corresponding SQL output.
:update                      Processes the schema and either update the database schema of EntityManager Storage Connection or generate the SQL output.

Demo básica

Para este apartado, voy a utilizar una demo Sandbox (entorno de pruebas) prácticamente igual que la que usan en la documentación de Doctrine 2. En este artículo encontrarás el código de todos los ficheros necesarios. Además, al final del mismo encontrarás un enlace para poder descargarlo.

El proyecto consiste de sólo 4 ficheros:

  • Entities/Users.php – Modelo de usuarios
  • Entities/Address.php – Modelo de dirección
  • cli-config.php – Fichero PHP que contiene el código de inicialización necesario para usar la utilidad en linea de comandos ‘doctrine’. Cada vez que usemos la utilidad ‘doctrine’, tendremos que tener este fichero.
  • index.php – Código de inicialización típico de una aplicación web que utilice Doctrine 2.

Vamos a echar un vistazo a los ficheros. En primer lugar veamos cómo es el archivo cli-config.php:

<?php
// (1) Autocargamos clases
require_once 'Doctrine/Common/ClassLoader.php';
$classLoader = new \Doctrine\Common\ClassLoader('Doctrine');
$classLoader->register();
$classLoader = new \Doctrine\Common\ClassLoader('Entities', __DIR__);
$classLoader->register();
$classLoader = new \Doctrine\Common\ClassLoader('Proxies', __DIR__);
$classLoader->register();

// (2) Configuración
$config = new \Doctrine\ORM\Configuration();

// (3) Caché
$cache = new \Doctrine\Common\Cache\ArrayCache();
$config->setMetadataCacheImpl($cache);
$config->setQueryCacheImpl($cache);

// (4) Driver
$driverImpl = $config->newDefaultAnnotationDriver(array(__DIR__."/Entities"));
$config->setMetadataDriverImpl($driverImpl);

// (5) Proxies
$config->setProxyDir(__DIR__ . '/Proxies');
$config->setProxyNamespace('Proxies');

// (6) Conexión
$connectionOptions = array(
'driver' => 'pdo_sqlite',
'path' => 'database.sqlite'
);

// (7) EntityManager
$em = \Doctrine\ORM\EntityManager::create($connectionOptions, $config);

// (8) HelperSet
$helperSet = new \Symfony\Component\Console\Helper\HelperSet(array(
'db' => new \Doctrine\DBAL\Tools\Console\Helper\ConnectionHelper($em->getConnection()),
'em' => new \Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper($em)
));

¡Que nadie se asuste! Realmente es un código muy sencillo, y prácticamente siempre se usará algo igual o muy parecido. Para superar el medio inicial, vamos a ver detalladamente qué es lo que ha pasado. El proceso de inicialización consta básicamente de dos pasos: Por una parte hay que asegurarse de que las clases de Doctrine pueden cargarse, y por otra hay que crear una instancia del Entity Manager. Siguiendo paso a paso:

  1. En primer lugar autocargamos las clases necesarias (nota: para esto podríamos haber utilizado el autocargador de otro framework). En concreto autocargaremos 3 secciones:
    1. El propio Doctrine, que trae Common, DBAL, ORM y Symfony. Si lo hemos instalado con PEAR, podremos verlo en el directorio /usr/share/php/Doctrine.
    2. Entities, que es el directorio en el que almacenaremos nuestros modelos.
    3. Proxies, otro espacio que es necesario definir. Los proxies son subclases de nuestros modelos que el propio Doctrine crea automáticamente.
  2. Una vez que nos hemos asegurado de que todas las clases pueden cargarse automáticamente, comenzamos a crear nuestro objeto EntityManager. Para ello, lo primero es crear un objeto de tipo Configuration.
  3. El primer parámetro que vamos a configurar es la caché. Podríamos haber usado APC, el sistema de cachés de facto de PHP (aconsejable en un entorno en producción), pero para pruebas usaremos el método ArrayCache.
  4. A continuación establecemos el driver con el que mapearemos nuestra base de datos. Esto es necesario para  hacer que nuestros modelos se correlacionen con nuestras bases de datos. Existen tres drivers disponibles:
    1. XML, desde donde se puede definir como encajar el modelo con la base de datos mediante sintaxis XML.
    2. YAML, al igual que con XML, también se puede definir utilizando YAML.
    3. Anotaciones, que en mi opinión son una maravilla. Mediante etiquetas en comentarios PHP defines cómo se mapean los datos. Más abajo, cuando lleguemos a los modelos, veremos un ejemplo de uso.
  5. Ahora configuramos los proxies. Simplemente establecemos su directorio, y su espacio de nombres.
  6. Conexión. Es aquí donde establecemos dónde se se encuentra nuestra base de datos. Si ésta no existiera, Doctrine la crearía por nosotros. En nuestro ejemplo usaremos SQLite.
  7. Ya está todo listo: hemos configurado la Caché, el Driver para mapear, los proxies y la conexión. Ya podemos crear el objeto EntityManager.
  8. Por último, creamos un objeto de tipo HelperSet, que es el que utilizará la utilidad de línea de comandos para saber cómo conectarse. El objeto HelperSet debe tener dos helpers: db, para identificar la conexión, y em, el EntityManager.

Ya tenemos listo el fichero cli-config.php, que como dijimos, es el que se utiliza para conectar la utilidad de consola ‘doctrine’ con nuestro proyecto.

Ahora vamos a ver los modelos.

Entities/Adress.php
<?php
namespace Entities;

/** @Entity @Table(name="addresses") */
class Address
{
   /**
    * @Id @Column(type="integer")
    * @GeneratedValue(strategy="AUTO")
    */
   private $id;

   /** @Column(type="string", length=255) */
   private $street;

   /** @OneToOne(targetEntity="User", mappedBy="address") */
   private $user;

   public function getId()
   {
      return $this->id;
   }

   public function getStreet()
   {
      return $this->street;
   }

   public function setStreet($street)
   {
      $this->street = $street;
   }

   public function getUser()
   {
      return $this->user;
   }

   public function setUser(User $user)
   {
      if ($this->user !== $user) {
         $this->user = $user;
         $user->setAddress($this);
      }
   }
}
Entities/User.php
<?php
namespace Entities;

/** @Entity @Table(name="users") */
class User
{
   /**
    * @Id @Column(type="integer")
    * @GeneratedValue(strategy="AUTO")
    */
   private $id;

   /** @Column(type="string", length=50) */
   private $name;

   /**
    * @OneToOne(targetEntity="Address", inversedBy="user")
    * @JoinColumn(name="address_id", referencedColumnName="id")
    */
   private $address;

   public function getId()
   {
      return $this->id;
   }

   public function getName()
   {
      return $this->name;
   }

   public function setName($name)
   {
      $this->name = $name;
   }

   public function getAddress()
   {
      return $this->address;
   }

   public function setAddress(Address $address)
   {
      if ($this->address !== $address) {
         $this->address = $address;
         $address->setUser($this);
      }
   }
}

No me detendré a explicar cómo se definen los modelos. En la propia documentación de Doctrine puedes ver una introducción al mapeo. Lo importante aquí es que, mediante etiquetas en los comentarios, hemos definido la relación entre la entidad User y la entidad Address, y también hemos definido cada una de sus columnas (¡hasta podemos indicar que una columna sea autoincremental!).

Llegados a este punto, tenemos 3 ficheros ya creados: cli-config.php, Entity/User.php y Entity/Address.php. Desde el raíz de nuestro proyecto vamos a ejecutar lo siguiente:

$ doctrine orm:schema-tool:create

Si todo ha ido bien, nos saldrá un mensaje indicándonos que el esquema de base de datos se ha creado correctamente. Y si comprobamos el directorio, veremos que se ha creado un nuevo fichero, database.sqlite, que contiene la definición de nuestras dos tablas.

Usándolo en una aplicación web

Bien, ya hemos definido las entidades, hemos creado la base de datos, y sabemos cómo funciona el código de inicialización de Doctrine 2. Ahora vamos a ver el fichero index.php:

<?php

// (1) Autocargamos clases
require_once 'Doctrine/Common/ClassLoader.php';
$classLoader = new \Doctrine\Common\ClassLoader('Doctrine');
$classLoader->register();
$classLoader = new \Doctrine\Common\ClassLoader('Entities', __DIR__);
$classLoader->register();
$classLoader = new \Doctrine\Common\ClassLoader('Proxies', __DIR__);
$classLoader->register();

// (2) Configuración
$config = new \Doctrine\ORM\Configuration();

// (3) Caché
$cache = new \Doctrine\Common\Cache\ArrayCache();
$config->setMetadataCacheImpl($cache);
$config->setQueryCacheImpl($cache);

// (4) Driver
$driverImpl = $config->newDefaultAnnotationDriver(array(__DIR__."/Entities"));
$config->setMetadataDriverImpl($driverImpl);

// (5) Proxies
$config->setProxyDir(__DIR__ . '/Proxies');
$config->setProxyNamespace('Proxies');

// (6) Conexión
$connectionOptions = array(
    'driver' => 'pdo_sqlite',
    'path' => 'database.sqlite'
);

// (7) EntityManager
$em = \Doctrine\ORM\EntityManager::create($connectionOptions, $config);

// (8) Código de prueba
$user = new \Entities\User();
$address = new \Entities\Address();

echo 'Hola mundo!' . PHP_EOL;

// Introduce aquí el código que maneje $user y $address

Si nos fijamos, los 7 primeros puntos son exactamente iguales que en cli-config.php. En el punto 8 creamos dos objetos de nuestro modelo, ya listos para usar. ¡Pero cuidado! Antes de poder hacer cambios en la base de datos SQLite, debes asegúrate de que el usuario de tu servidor web tiene permisos de escritura en el fichero database.sqlite. Ahora podríamos añadir el siguiente código de ejemplo:

$address->setStreet("Calle Río tinto, 12");
$em->persist($address);
$em->flush();

echo "Insertamos dirección<br />" . PHP_EOL;

$user->setName("Pedro");
$user->setAddress($address);
$em->persist($user);
$em->flush();

echo "Insertamos usuario<br />" . PHP_EOL;

Simplemente tenemos que acceder desde nuestro navegador a ese fichero (o también mediante “php index.php”) para que PHP ejecute el código. Una vez hecho, podemos consultar la base de datos mediante sqlite3:

$ sqlite3 database.sqlite
SQLite version 3.6.22
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> select * from addresses;
1|Calle Río tinto, 12
sqlite> select * from users;
1|1|Pedro

¡Lo ha hecho todo él sólo por nosotros! Así de sencillo resulta trabajar con bases de datos con un ORM :) A partir de aquí empieza lo bueno: ya puedes guardar tus objetos de forma persistente en base de datos, sin tener preocuparte ni por crear las tablas, ni por SQL, ni nada. Céntrate en PHP :)

¡Gracias por leer!

Descargar el código fuente

Capturador de enlaces de Películas Yonkis

AVISO IMPORTANTE: Script obsoleto

Desde que el diseño de Películas Yonkis cambió, el script que desarrollé y que hay aquí colgado, se ha quedado obsoleto. Sería necesario reajustar las expresiones regulares. Ignoro si hay más cambios (como nuevas “trampas” en JavaScript).

Por favor, si has creado una versión actualizada, compártelo y añado una referencia a la URL donde lo tengas alojado.

¡Cuantísimo tiempo sin escribir! Por suerte o por desgracia, últimamente tengo muy poco tiempo para mí. Recientemente se han visto varias noticias sobre cómo dejar de depender de páginas de enlaces como Series Yonkis y compañía. Al hilo de ese asunto, alguien redactó el Proyecto Communis. En resumen consiste en crear una base de datos única con todos los enlaces a descargas. De esa base de datos podrán beber todos los servicios que lo deseen. En mi opinión, ése no es el camino a seguir; seguiría siendo un sistema centralizado, en tanto que tanto los ficheros como la base de datos estarían alojadas en algún servidor en particular. Es un sistema muy débil, pero es el que venimos usando ahora. La solución pasa por volver a un sistema completamente descentralizado como es el P2P.

Ahora bien, las páginas de descargas son hoy por hoy la solución más eficaz para acceder a los contenidos. En concreto, la web que más tráfico tiene (y entiendo que la mayor base de datos también) es Películas Yonkis. Y no sé a vosotros, pero a mí no me gusta, no me gusta nada. Está inundada de screeners, audios en latino, o versiones originales. No hay nada malo en ello, siempre y cuando hubiera alguna forma eficaz de poder filtrar contenidos. Y como no existe, pues me he hecho un programa (sí, otro más) para descargar su base de datos. Luego tengo una web de uso personal para yo (y mi señor padre) poder localizar esos enlaces.

Os presento mi script capturador de Películas Yonkis: crawler.php

Funciona igual que el que desarrolló Carlos Capote para Series Yonkis: Un script se encarga de descargar documentos web, y mediante expresiones regulares, captura su contenido. Finalmente, se salta la protección javascript ejecutando javascript en consola (con SpiderMonkey).

Características

Como principales características tiene:

  • Funciona en PHP, de modo que se puede ejecutar en prácticamente cualquier servidor web.
  • Es únicamente para Películas Yonkis, aunque está hecho de forma que sea fácilmente extensible para añadir otros portales (próximamente, CineTube).
  • Guarda una copia local en SQLite.
  • Puede ejecutarse en busca de nuevos enlaces. Sólo lleva unos segundos, ya que mediante peticiones HEAD comprueba si la URL ya se ha analizado: para ello, almacena una lista de URLs y sus cabeceras ETag y last-modified.
  • Evita duplicados, ya que almacena en base de datos cada URL de descarga.

Únicamente tiene dos dependencias: SpiderMonkey y cURL para PHP.

Base de datos

La base de datos tiene dos tablas, Movies y Movie_Links:

Movies:

  • id – identificador numérico.
  • title – Título de la película.
  • year – Valor numérico para el año.
  • description – Sinopsis.
  • genres – Lista separada por comas de géneros.

Movie_Links:

  • id – Identificador numérico.
  • movie_id – Identificador de la película a la que pertenece.
  • urls – Lista de URLs de esta descarga. En la mayoría de los casos será sólo una, pero hay películas que están divididas en partes.
  • lang – Idioma.
  • subs – Indica si tiene o no subtítulos.
  • quality – Calidad, de 1 a 5.
  • length – Duración en minutos.
  • enc – Método de codificación, por ejemplo DVD-Rip o Screener.
  • size – Tamaño total en MB.
  • format – Formato del fichero, por ejemplo AVI.
  • resolution – Resolución de salida.
  • type – Indica si es descarga directa o visualización online.
  • service – Almacena el servicio utilizado, por ejemplo, megaupload, megavideo, veoh, fileserve, etc.
  • broken – No usado todavía, sirve para notificar si un enlace está roto.

Y aquí dejo una copia de la base de datos (14.85MB). Puedes descargarla y ejecutar:
$ sqlite3 peliculas.db

Supongamos que queremos consultar todas las pelis de ciencia ficción que estén en megaupload, en DVD-Rip y en español de España, podríamos ejecutar:

$ sqlite3 peliculas.db
SQLite version 3.6.12
Enter SQL statements terminated with a ";"
sqlite> select distinct(m.title)
from movies m, movie_links ml where
ml.movie_id = m.id
and enc="DVD-Rip"
and genres like "%ciencia%"
and lang="spanish"
and service="Megaupload";

MySQL Workbench – Diseño de bases de datos

A menudo las aplicaciones necesitan almacenar información de forma persistente, de manera que pueda ser capturada más adelante. Cada vez son más las aplicaciones de escritorio que lo utilizan, al igual que aplicaciones web, donde los RDBMS más populares son Microsoft SQL Server y MySQL.Este últmo, es sin duda el sistema de bases de datos rey de Internet. Sin embargo, no sé si os pasará como a mí, que trabajar día a día con él a veces me resulta una tarea desorganizada y costosa. Para mejorar eso surge MySQL Workbench.

¿Pero para qué sirve este programa? Es una herramienta visual de diseño, modelado, creación y mantenimiento de bases de datos.

Hasta ahora yo venía funcionando con herramientas con las que, en conjunto, lograba un resultado similar. Para el modelado entidad relación utilizaba Dia, la aplicación de Gnome para la creación de diagramas, o Umbrello, la solución basada en KDE para modelar diagramas UML. Nunca me acabó de convencer ninguna de estas herramientas, aunque lograban su cometido (A veces veía casi más efectivo dibujar las cajitas directamente con un software de diseño vectorial). Posteriormente, bien de forma automática, bien a mano, generaba el código SQL que definía la base de datos. Finalmente, para el mantenimiento del día a día, empleaba mayormente phpmyadmin.

Muchas aplicaciones, muchos cambios de contexto, y mucha semántica que se pierde al pasar de una herramienta a otra. Mmmm eso no mola. Buscando, encuentro una aplicación que hace tiempo que había visto, pero que había olvidado por completo. Efectivamente, me refiero a MySQL Workbench :) Surge como evolución natural de DBDesigner, de FabForce, pues el equipo de desarrollo está liderado por el fundador de DBDesigner, Michael G. Zinner.

Cosas chulas que podemos hacer desde MySQL Workbench:

  1. Ingeniería inversa de DDL de MySQL. A partir del código SQL, sin mediar el servidor de bases de datos, podemos visualizar toda la información en forma de tablas y diagramas generados automáticamente.
  2. Forward engineer, que lógicamente nos permite crear el nuevo código SQL.
  3. Conexión directa con el SGBDR. Podemos incluso sincronizar modelos.
  4. Soporta triggers y procedimientos almacenados.
  5. Diagramas bonitos y personalizables (Por ejemplo, que los únicos campos mostrados sean las claves).

A continuación, pongo un ejemplo de diagrama generado utilizando la base de datos de ejemplo sakila.

esquema de sakila

¿Dónde consigo esta maravilla? Te estarás preguntando. Sígueme… ¿Y dónde funciona? En Windows, Mac OS X y por supuesto Linux. Por cierto, una pega que le veo: Utiliza GTK++ ;-)

Aviso para usuarios de Ubuntu: Existen paquetes .deb de la versión 5.1 Beta disponibles listos para instalar en Ubuntu. Yo estoy ejecutando Kubuntu 9.04 y me han dado problemas de tipo Segmentation Fault en libglade. He probado a compilar e instalar la versión 5.2 Alpha y por el momento todos los errores han desaparecido.