Modelado de datos — A mi estilo :)

Siguiendo a mi post sobre MVC hecho en casa, que en mi humilde opinión fue útil, ya que hubo muchos comentarios y correos electrónicos sobre el tema, esta vez vengo con algo relacionado, que es sobre una clase sencilla para manejar los datos sin escribir SQL (o escribiendo lo menos posible), siempre teniendo en cuenta eficiencia, simpleza y extensiblidad.

Seguramente se preguntaran porque implementar algo que ya hay bastante, y la verdad que probé casi todos los existentes (no puedo dar nombre ni criticas porque conozco personalmente a algunos miembros de esos proyectos) y ninguno lleno mis expectativas, principalmente porque eran muy complicados a la hora de hacer consultas complicadas (varias consultas anidas, consultas con varias tablas, etc). Otra cosa que no me gustó es que crea todas las tablas en base a la definiciones en algún formato, en realidad es una característica muy útil, pero tiene un costo muy alto, que son muchas consultas (imagínese tener 100 tablas, realizar 100 consultas solo para hacer diffs). Todas esas características, tienen un costo bastante algo, que es mucho código en memoria por cada consulta, aunque exista métodos para alivianar de alguna manera esta ultima desventaja.

Mientras navega leyendo códigos, tratando de tener un idea de que hacer algo mas sencillo, de pronto al leer (para fines educativos) los fuentes de Menéame (donde tengo algunos mínimos e insignificantes aportes), pareció muy práctica la abstracción de la base de datos, por ejemplo la manera de como se abstrae la tabla user, no solo el acceso/modificación de la tabla User estan ahí, sino también todo lo relacionado con el usuario, lo cual hace todo bastante ordenado.

Tomando como base las clases de acceso de datos, más que nada el API en general, ninguna porción de código fue copiado ya que no podría por cuestiones legales (soy fanático de las licencias mas liberales, BSD o similares) comencé un proyecto llamado Simple ORM. Básicamente es un ORM bastante simple, no crea las tablas en base de una definición, tampoco hace lo inverso (osea generar codigo en base a tablas existentes), simplemente es una abstracción de los datos con un plus muy importante que es el cacheo de los mismo para optimizar (totalmente opcional). Esta construida sobre PDO, que agrega otra interfaz hacia la base de datos en sí, la cual al ser nativa (escriba en C) es bastante eficiente, y además emula transacciones y SQL preparadas con variables (útil para evitar SQL injections).

La verdad que hablar/escribir mucho no me gusta, así que vamos a la parte divertida, el código. La conexión   es bastante sencilla, casi como lo haríamos normalmente:

<?php
require "DB.php";

DB::setUser("root");
DB::setPass("foobar");
DB::setDB("testing");
DB::setDriver("mysql"); //Por defecto

Como vieron, nada de otro mundo, además nótese que la conexión no es realizada en este punto, sino que al ser necesitado, esto es así porque existe la probabilidad que la consulta a realizar ya este en el Cache. Ahora supongamos que contamos con la siguiente tabla SQL:

Simple DB example

Simple DB example

A continuación una simple representación de la base de datos utilizando SimpleORM:

<?php
class SessionModel extends DB
{
    public $startdate;
    public $endate;
    public $user;

    function user_filter(&$user)
    {
        if (!$user InstaceOf User) {
            $user = $user->ID;
        } else {
             $uobj = new User;
             $uobj->ID = $user;
             if (!$uobj->load()->valid()) {
                 throw new Exception("Invalid user");
             }
        }
    }

    function getTableName()
    {
        return "session";
    }
}

class User extends DB
{
    public $username;
    public $passwd;

    function passwd_filter(&$pass)
    {
        $pass = md5($pass);
   }

    function username_filter(&$username)
    {
        $oUsername = $this->getOriginalValue('username');
         if ($oUsername && $username != $oUsername) {
             throw new Exception("You can't modify your username");
         }
    }
}

Como vieron las clases son sencillas, solo necesitan declararse explícitamente las propiedades con el mismo nombre de las columnas (no necesariamente, esto puede cambiarse re-implementando el método relations — no está muy probado todavía) y nada mas. El filtrado/validación de los datos se realiza mediante la declaración de nuevo métodos con un nombre especial (nombre propiedad del objeto + “_filter”). Al ser de esta manera los filtros son bastante maleables, solo miren los métodos SessionModel::user_filter(), User::username_filter() y User::password_filter(), donde notamos que los filtros pueden modificar el dato a ser guardado o pueden detener la ejecución lanzando una excepción. También por defecto las clases tienen que tener el mismo nombre que las tablas o de lo contrario hay que redefinir el método getTableName().

Para realizar consultas, la clase en sí es muy limitada, ya que no provee ordenamiento, ni limitaciones (por ahora) de ningún tipo. Simplemente provee una interfaz amigable para las iteraciones, alteraciones e inserciones de datos.

$user = new User;
$user->username = "foobar";
/* "password" es transformado a su md5 antes del select [usea load()] */
$user->password = "password";
if ($user->load()->valid()) {
     /* Inserta un nuevo campo en la tabla session */
     $session = new SessionClass;
     $session->user = $user;
     $session->startdate = date("Y-m-d H:i:s");
     $session->save();
}

/* Ejemplo sencillo de Iteracion */
$sessions = new SessionClass;
$sessions->user = $user;
/* de este modo */
$sessions->load();
foreach ($sessions as $session) {
    /* hacer algo con la sesion */
}
/* o */
foreach ($sessions->load() as $session) {
    /* hacer algo con la sesion */
}

Hasta aquí nada fuera de lo común, y la verdad poco útil, ya que en la vida real se necesitan mucho mas que simples operaciones, y he aquí la parte que me gusta, como hacer cosas complicadas, y la verdad que la mejor manera (mas sencillo, mas eficiente) de hacer las cosas complicadas es usando SQL en sí, nada de otros sub-lenguajes de manipulación de datos, ni tantos objetos y métodos para formar una consulta. Hablando en ejemplos prácticos, supóngase que necesita un listado de los usuarios con más de 5 sesiones en un rango variable de fechas, ¿como lo haríamos?

/* Agregamos este método a la clase User */
function & getMostActivedUsers($min, $max) {
     $sql = <<<EOT
SELECT u.* FROM users u WHERE u.id in (
     SELECT id FROM session WHERE startdate > :idate && enddate < :edate
     GROUP BY id HAVING count(*) > 5
)
EOT;
     $this->setDataSource($sql, array("idate" => $min, "edate" => $max), true);
     return $this; /* opcional para ahora una linea al iterar */
}

/* Iteración de ejemplo donde también se muestra un alteración de los datos */
/* (no contemplada en mi modelo) */
$users = new User;
foreach ($users->getMostActivedUsers($min, $max) as $user) {
      /* Los datos tambien pueden ser accedidos como Arrays */
      $user['level'] = 'god';
      /* Si el ultimo parametro de setDataSource es false (por defecto) */
      /* esto lanzaría una excepción (util para consultas anidadas)       */
      $user->save();
}

Como vieron, es muy sencillo de extender, y es lo mas manual posible solo se necesita conocer bastante bien de SQL, siguiente a mi frase “mientras mas fácil es para los programadores, mas difícil para el servidor”(referente a los frameworks que ya hacen todo ;) )

La limitación mas notoria que me viene a esta hora (6:24 am) a la cabeza es que la tabla necesita tener una columna “id” ($obj->ID). Este puede ser auto-incrementado (recomendado) o puede ser otro valor. Esto no limita a tener tablas sin “id” (ejemplo notorio tablas auxiliares para guardar las relaciones de tablas n:n), que pueden ser implementados como métodos de algunas clases, prometo que escribiré lo mas pronto posible ejemplos mas complicado y como representarlo con SimpleORM.

En próximas semanas estaré escribiendo mas ejemplos sobre el uso de SimpleORM, y eventualmente seguiré modificando el proyecto y agregando nuevas características.

2 Comments

  1. Matías says:

    ¡Se ve excelente!
    A probarlo…

  2. Excelente como siempre César! espero el siguiente post (Y)

Leave a Reply