Object Relational Mapping en PHP5 (capitulo 1)

Introducción:

Hace algún tiempo andaba persiguiendo la idea, el concepto de ORM (Object Relational Mapping) implica trasladar la lógica relacional de una tabla:
TABLA -> CAMPOS -> Registros
a un lógica de objetos, donde cada tabla sea representada por una clase, cada campo (atributo) sea representado por una propiedad de dicha clase y cada método represente una operación posible a realizar sobre dichos datos.
El concepto de capa de datos está tan estrechamente relacionado al concepto de Base de datos, que realmente indicar que se puede programar con nulo SQL es obviar la potencia de SQL (y restringir en muchas formas a la capa de datos); de hecho, le leído por ahi que grandes aplicaciones hechas con este concepto de abstracción, han cambiado a usar nuevamente SQL en su capa de datos y es que de hecho, RoR será todo lo que quieran, pero principalmente es … lento …
volviendo al tema de ORM, la lógica de objetos implicada en el acceso a los datos deviene de perl::dbi (aunque los de ruby digan que su invento el activerecord es genialidad propia) y consiste en:
una clase por tabla
class inscritos :
cada atributo (propiedad) representa un campo de la DB:
inscritos.nombre, inscritos.apellido, inscritos.cedula

y cada método, una operación realizable a la DB:

inscritos = new activerecord ('inscritos');

inscritos->nombre = 'Jesus'

inscritos->apellido = 'Lara'

inscritos->cedula = '13264658'

inscritos->fecha_inscrito = date()

inscritos->save() //método que me permite guardar el registro en la DB.

Como se ve, inscritos es un OBJETO y dentro de él contenemos el “mapa fisico” (lo que en teoría se conoce como la metadata de la tabla) de la tabla, con sus relaciones, indices primarios, tipos de datos, etc.

Hasta el momento existian 2 posibilidades conocidas (y un sinfin de desconocidos, aqui va otra más) para operar ORM en PHP, una de ellas PROPEL, este permitía tener un mapa completo de la tabla, pero debia ser “creado” usando un estilo “alternativo” de XML llamado YAML; si la tabla cambiaba de forma, habia que destruir el YAML y hacer uno nuevo, uno por cada tabla (no muy práctico tomando en cuenta que RoR hace este mapeo “on fly”).
el otro, CREOLE, es más una capa de abstracción que un ORM en sí …
CakePHP agrega su aproximación a un ActiveRecord con todas las limitaciones que un código PHP4 puede tener, entre ellos que te exige una forma “particular” de tener la tabla (como por ejemplo, un id en cada tabla para que pueda “identificar” quien es el PK (primary key) de la tabla); esto tiene algunas consecuencias, una tabla como esta:

CREATE TABLE  `ejemplos_ajax`.`dpt_parroquias` (

`id_entidad` int(8) unsigned NOT NULL,

`id_municipio` int(8) unsigned NOT NULL,

`id_parroquia` int(8) unsigned NOT NULL,

`parroquia` varchar(75) DEFAULT NULL,

`codigoe` char(3) NOT NULL,

`capital` varchar(128) DEFAULT NULL,

PRIMARY KEY (`id_entidad`,`id_municipio`,`id_parroquia`)

) ENGINE=InnoDB DEFAULT CHARSET=latin1

Con un indice PK compuesto de 3 campos no es entendible por CakePHP (y en sistemas complejos son muchas más las tablas “extrañas” que las básicas tablas de blog id, nombre, apellido …).

Decidi hacer esta saga de “Haga su ORM en PHP5 en algunos pasos” … (y despues comase una pizza) …

Una idea inicial de “que debe hacer el ORM”:

Lo que perseguimos es llegar a un código (yo ya llegué, jijiji, lo iré publicando en seguidilla en esta serie de posts) que sea PHP5-only y con la potencia suficiente para realizar cosas como:

//cargar un activerecord de la tabla dpt_entidades

$orm = $db->activerecord('dpt_entidades');//decirle que se traiga TODOS los datos

$result = $orm->find_all();

//e iterar sobre ellos elegantemente en una consulta objeto $result->campo y no via array $result[array]

//usando do while

do {

echo $result->cod_entidad . ' ' . $result->entidad . ' ' . $result->capital . '<br/>';

} while($result->next());

//o usando foreach

foreach ($result as $value) {

echo $value->cod_entidad . ' ' . $value->entidad . ' ' . $value->capital . '<br/>';

}

Tambien crear filtros y sentencias más complejas:

//condicion between

$orm->cod_entidad->between('FAL', 'MON');

//ordenar:limit and offset (method chaining):

$orm->order_by('cod_entidad')->limit(10)->offset(0);

//donde el campo mostrar sea igual a uno

$orm->mostrar->where(1);

//y el campo entidad sea igual a Monagas O igual a Lara o distinto de Nulo

esto de aqui abajo, se conoce como "method chaining" y consiste en encadenar uno tras otro los métodos comunes, es una de las posibilidades de PHP5

$orm->entidad->where('Monagas')->where('Lara', true)->where(null, true);

//y por favor ordenalo por entidad

$orm->entidad->sort();

tambien puedes hacer LIKE

$orm->entidad->like('L')->like('M', 1,true);

o estructuras como IN, EXISTS y otras de las condiciones WHERE en SQL

$cod_entidad = array('DCA', 'MON', 'LAR', 'ZUL');

$orm->cod_entidad->in($cod_entidad)->in('SELECT cod_entidad FROM dpt_ciudades', true, true);//pido un ORM de la tabla dpt_ciudades

$ciudades = $db->activerecord('dpt_ciudades');

//indico que campos deseo en la consulta

$ciudades->select('id_entidad, count(*) as cantidad');

//y agrupo por el campo id_entidad

$ciudades->id_entidad->groupby();

//ademas, creo una condicion group (having)

$ciudades->having('count(*)', '>30'); //solo aquellas entidades con más de 30 ciudades

Tambien podemos ver algo mas complejo como:

$ciudades->select('DISTINCT e.entidad,  e.id_entidad, ciudad');

$ciudades->id_entidad->join('dpt_entidades e', 'e.id_entidad', 'LEFT');

$ciudades->limit(150);

$ciudades->id_entidad->between(6, 8);

/* esto causa una sentencia como:

SELECT DISTINCT e.entidad, e.id_entidad, ciudad FROM dpt_ciudades LEFT JOIN dpt_entidades e ON (dpt_ciudades.id_entidad=e.id_entidad) WHERE dpt_ciudades.id_entidad BETWEEN '6' AND '8' LIMIT 150

*/

Tambien se pueden operar los datos:

$entidad = $orm->create();

$entidad->cod_entidad = 'PDF';

$entidad->entidad = 'Principado del Distrito Federal';

$entidad->capital = 'Elbonia';

$entidad->save(); //al ser un nuevo registro, se inserta en la base de datos

echo "la ultima entidad guardada fue {$entidad->last_id}";

Donde se ha visto?

CodeIgniter (un framework para PHP4/5) tiene un conjunto de funciones que se aproximan a la comodidad de crear sentencias complejas como SQL de arriba pero usando solo métodos de objeto (de hecho, codeIgniter es donde he sacado muchas de las ideas), el único inconveniente de CodeIgniter es que usa a PROPEL, por ende, hay que crear los mapas de las tablas a mano usando YAML.
Sin embargo, todas las extensiones de conexión a datos de PHP5 soportan entregar metadata con las consultas, y al estilo RoR es simplemente hacer algo (internamente) como:
$result = $this->_db->query(“SHOW COLUMNS FROM {$tabla}”);
y ese SHOW COLUMNS devolvera toda la metadata necesaria para recrear la estructura de la DB dentro de un objeto.
simple no?

Dormir y Despertar:

No tanto, leer y parsear la DDLde cada tabla es un proceso lento (aunque en PHP5 no es tan severamente lento como RoR); sin embargo, aqui otra característica de PHP5 corre en nuestro auxilio, existe la posibilidad de “serializar” un objeto PHP5 en forma de una cadena binaria y guardarla en disco o en un DB, haciendo caché de la metadata y salvando milisegundos preciosos para nuestra aplicación.
Nota: para serializar un objeto se procede de la siguiente manera:
$inscritos = $db->activerecord(‘inscritos’);
$cache = serialize($inscritos);

y se procede a guardarlo en una DB o en un archivo en disco, algo como:
file_put_contents(‘cache_inscritos.tmp’, $cache);

para restaurarlo se hace:
$cache = file_get_contents(‘cache_inscritos.tmp’);
y luego se le “restaura” a la forma objeto:
$inscritos = unserialize($cache);

Y como se restaura automáticamente la conexión?, es decir, los recursos (conexiones a DB, sockets, etc) son cosas inserializables, entonces PHP5 pone a nuestra disposición los métodos mágicos __sleep y __wakeup

public function __sleep() {
$this->connection->close();
}
__sleep se ejecuta previo a la serializacion de un objeto, te permite limpiar variables temporales para no ocupar espacio, cerrar conexiones a la DB, etc

luego
public function __wakeup() {
$this->connection->connect();
}

el metodo __wakeup se ejecuta cuando un objeto es “despertado” de una serialización, en este caso, podemos volver a conectarnos a la DB, revisar si todo está correcto, etc.

Y en la proxima …

En la próxima entrega explicaré como lograr iterar usando foreach sobre un objeto complejo y sobre como lograr acceder a los campos usando notación de objeto ($inscritos->nombre, $inscritos->cedula, etc).

Acerca de phenobarbital

http://about.me/phenobarbital

Publicado el 14 abril 2007 en PHP, PlanetaLinux, Programacion. Añade a favoritos el enlace permanente. 6 comentarios.

  1. Echale una revisada a Class::DBI, DBIx::Class, Rose::DB::Object .. esto se ha hecho en Perl desde hace años .. Pero es bueno que también lo tengan en PHP😀

  2. Grax; de hecho es de Class::DBI de donde he visto la funcionalidad plena; los de RoR pregonan que su solución de ActiveRecord es la crema que le faltaba al café, sin tomar en cuenta que este tipo de soluciones no solo se pueden realizar en cualquier lenguaje (si lo conoces bien); sino que ademas, se viene haciendo en lenguajes con una OO más madura como python o perl; asi que RoR es más un lenguaje con mercadotécnia .NET

  3. La OO de PHP5 no tiene más de 2 años, pero veo que lleva un buen camino (solo faltan los prometidos namespaces de la versión 6 para declararse como una OOP seria).

  4. Una nota adicional: lo que le falta a DBIx::Class es la posibilidad de ella misma leer la metadata de la tabla que uno escoja (y así descubrir que campos tengo, cuales son mis PK, mis FK, mis Index, etc) y no tener que hacer cosas como:
    __PACKAGE__->table(‘artist’);
    __PACKAGE__->add_columns(qw/ artistid name /);
    __PACKAGE__->set_primary_key(‘artistid’);

    y es algo que es facil, nunca he entendido por qué Class::DBI no lo hace (descubrir ella misma on-fly la metadata de la tabla).

  5. Bueno, eso de solo decirle al script los datos de conexión y que te de la información de todas las tablas que hauy en la BD asi como sus componentes se puede hacer, hay varios módulos que hacen, no sé si este es algo como lo que necesites:
    http://search.cpan.org/~blblack/DBIx-Class-Schema-Loader-0.02007/
    y pues hay bastantes módulos en CPAN, haz una búsqueda con “dbix” y consigues de todo un poco ..😉

  6. DBIx::Class::Schema::Loader se ve interesante, pero tiene un fallo bastante extraño, la actitud del autor:
    “The relationships generated by this module will probably never be
    as well-defined as hand-generated ones.”; por que?, yo nunca he conseguido una tabla lo suficientemente compleja como para “desconfiar” de la metadata entregada por una conexión a una DB, el concepto de crear el modelo de la tabla a mano es algo que en un RAD se debería saltar; lo que se busca, es un comportamiento como DBIx::ORM::Declarative; pero, evidentemente, sin tener que crear “a mano”
    table => $tabname,
    primary => [ $key, … ],
    unique => [ [ $key, …], … ],
    columns =>

    el schema table; creo que la mejor explicación a ello lo consigues en el paquete Rose::DB::Object::Metadata; fijate:
    “Manual population of metadata objects can be tedious and repetitive. Nearly all of the information stored in a Rose::DB::Object::Metadata object exists in the database in some form. It’s reasonable to consider simply extracting this information from the database itself, rather than entering it all manually. This automatic metadata extraction and subsequent Rose::DB::Object::Metadata object population is called “auto-initialization.””.
    En este caso, si de crear un RAD funcional, rápido e inteligente, Rose::DB::Object es para mi parecer, uno de los más efectivos ORM presentes en CPAN.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

A %d blogueros les gusta esto: